diff --git a/.githooks/pre-push b/.githooks/pre-push index 9e98445..8125d04 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -112,7 +112,7 @@ echo "" echo "๐ŸŽฎ [4/4] Godot Script Parse Check..." if command -v "$GODOT_BIN" &>/dev/null; then "$GODOT_BIN" --headless --editor --quit --path . 2>&1 | tee tools/godot_lint.log - if grep -iE "SCRIPT ERROR|Parse Error|Compile Error|hides an autoload singleton|SHADER ERROR" tools/godot_lint.log; then + if ! python3 tools/filter_godot_errors.py tools/godot_lint.log; then echo "" echo " โŒ Godot reported script/shader errors. Fix before pushing." exit 1 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4513f32..45f622f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,8 +4,7 @@ Please ensure all items are checked before submitting this PR for review. ## 1. Automated Testing - [ ] **Deterministic Math Validation**: Run `python3 tests/validate_math.py`. -- [ ] **In-Engine Math Validation**: Run `godot --script tests/test_deterministic_math.gd --headless`. -- [ ] **Full Test Suite**: Run `bash tests/run_all_tests.sh`. +- [ ] **Full Test Suite**: Run `bash tools/pre_push_check.sh` (which executes GdUnit4 tests). ## 2. Pre-Commit Hooks - [ ] **GDScript Format**: Run `gdformat scripts/ tests/ ui/`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a6c06e..8227b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,9 @@ name: CI +permissions: + contents: read + checks: write + pull-requests: write on: push: branches: [main] @@ -79,28 +83,25 @@ jobs: run: | mkdir -p tools godot --headless --editor --quit --path . 2>&1 | tee tools/godot_lint.log - if grep -iE "SCRIPT ERROR|Parse Error|Compile Error|hides an autoload singleton" tools/godot_lint.log; then + if ! python3 tools/filter_godot_errors.py tools/godot_lint.log; then echo "โŒ GDScript linting failed." exit 1 fi echo "โœ… GDScript lint passed." - - name: Run Full Test Suite - run: | - chmod +x tests/run_all_tests.sh - tests/run_all_tests.sh 2>&1 | tee tests/test_output.log + - name: Run GdUnit4 Tests + uses: MikeSchulze/gdUnit4-action@v1 + with: + godot-version: '4.2.2' + version: 'v4.4.3' + paths: 'res://tests' + publish-report: true + report-name: 'gdUnit4-report' - name: Upload Test Results uses: actions/upload-artifact@v7 if: always() with: name: test-results path: | - tests/test_output.log - tests/validate_math_report.json tools/godot_lint.log - - name: Report Test Summary - if: always() - run: | - echo "### Test Results" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - tail -20 tests/test_output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY + tests/validate_math_report.json + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 310e986..a5f171d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,9 +43,10 @@ jobs: - name: Regenerate asset cache run: godot --headless --import --path . - name: Run Godot test suites - run: | - chmod +x tests/run_all_tests.sh - tests/run_all_tests.sh + uses: MikeSchulze/gdUnit4-action@v1 + with: + godot-version: '4.2.2' + paths: 'res://tests' - name: Export Windows build run: | diff --git a/AGENTS.md b/AGENTS.md index 1f23517..4b7d06e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -198,8 +198,8 @@ CombatRoom (Node2D) ## Testing Commands ```bash -# Run all tests -bash tests/run_all_tests.sh +# Run all tests via GdUnit4 +godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a tests/ --ignoreHeadlessMode # Run specific test godot --headless --path . -s tests/test_entity_lifecycle.gd @@ -312,7 +312,7 @@ GitHub Actions (`.github/workflows/ci.yml`) runs **exactly the same checks** as |---|---| | `gdscript-format` | `gdformat`, `markdownlint`, and `json.tool` | | `validate-math` | `python3 tests/validate_math.py` | -| `gdscript-lint` | Godot headless editor scan + `tests/run_all_tests.sh` | +| `gdscript-lint` | Godot headless editor scan + GdUnit4 test suite | > **CI uses `--check` only (no auto-fix).** Auto-fixing happens locally via hooks. > If CI fails on formatting, it means the pre-push hook was bypassed โ€” run `bash tools/setup_hooks.sh` to re-install hooks. @@ -347,8 +347,7 @@ GitHub Actions (`.github/workflows/ci.yml`) runs **exactly the same checks** as 1. Create `tests/test_feature.gd` 2. Use strict typing throughout -3. Include in `tests/run_all_tests.sh` -4. Run `bash tests/run_all_tests.sh` to verify +3. Run `godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a tests/test_feature.gd` to verify ### Debugging Tips diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a25cf7..41b2b60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,11 +127,11 @@ func _config_int(key: String, fallback: int) -> int: ## Running Tests ```bash -# Full test suite (mirrors CI) -bash tests/run_all_tests.sh +# Full test suite via GdUnit4 (mirrors CI) +godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a tests/ --ignoreHeadlessMode # Single test file -godot --headless --path . -s tests/test_deterministic_math.gd +godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a tests/test_deterministic_math.gd # Python cross-platform math validator python3 tests/validate_math.py @@ -148,11 +148,10 @@ bash tools/pre_push_check.sh ## Adding a Test 1. Create `tests/test_.gd`. -2. Use `extends SceneTree` (with `_initialize()`) for standalone tests. +2. Use `extends GdUnitTestSuite`. 3. Use strict typing throughout. -4. Exit with `quit(0)` (pass) or `quit(1)` (fail). -5. Add it to `tests/run_all_tests.sh`. -6. Run the full suite to confirm no regressions. +4. Use `assert_that()` for assertions. +5. Run the suite locally or headlessly to confirm no regressions. --- diff --git a/addons/gdUnit4/LICENSE b/addons/gdUnit4/LICENSE new file mode 100644 index 0000000..8c60d13 --- /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 0000000..cae9138 --- /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 0000000..9976d53 --- /dev/null +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid @@ -0,0 +1 @@ +uid://cexanuy5w4ns4 diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd new file mode 100644 index 0000000..8b22805 --- /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("", "")\ + .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..d213f77 --- /dev/null +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid @@ -0,0 +1 @@ +uid://dx3gbguvyfdjn diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg new file mode 100644 index 0000000..236abc8 --- /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.2.0-rc1" +script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd new file mode 100644 index 0000000..d91b50f --- /dev/null +++ b/addons/gdUnit4/plugin.gd @@ -0,0 +1,100 @@ +@tool +extends EditorPlugin + +var _gd_inspector: Control +var _gd_console: Control +var _filesystem_context_menu: Variant +var _editor_context_menu: Variant +var _editor_code_context_menu: Variant + + +func _enter_tree() -> void: + var inferred_declaration := GdUnitSettings.validate_is_inferred_declaration_enabled() + if inferred_declaration.is_error(): + printerr(inferred_declaration.error_message()) + 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() + GdUnitEditorColorTheme.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: + # Only add context menus on Godot 4.5+ + if Engine.get_version_info().hex < 0x40500: + return + + _filesystem_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd").new() + _editor_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd").new() + _editor_code_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd").new() + if self.has_method("add_context_menu_plugin"): + self.call("add_context_menu_plugin", 0, _filesystem_context_menu) + self.call("add_context_menu_plugin", 1, _editor_context_menu) + self.call("add_context_menu_plugin", 2, _editor_code_context_menu) + + +func _remove_context_menus() -> void: + # Only remove context menus on Godot 4.5+ + if Engine.get_version_info().hex < 0x40500: + return + + if self.has_method("remove_context_menu_plugin"): + if is_instance_valid(_filesystem_context_menu as Object): + self.call("remove_context_menu_plugin", _filesystem_context_menu) + if is_instance_valid(_editor_context_menu as Object): + self.call("remove_context_menu_plugin", _editor_context_menu) + if is_instance_valid(_editor_code_context_menu as Object): + self.call("remove_context_menu_plugin", _editor_code_context_menu) + + +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..25a966e --- /dev/null +++ b/addons/gdUnit4/plugin.gd.uid @@ -0,0 +1 @@ +uid://2dkda50cgvnt diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd new file mode 100644 index 0000000..bc00a4e --- /dev/null +++ b/addons/gdUnit4/runtest.cmd @@ -0,0 +1,65 @@ +@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. +:: --remote-debug tcp://127.0.0.1:0 prevents Godot from activating its local interactive +:: CLI debugger, which would cause an endless 'debug>' loop on script parse errors. +:: Port 0 is used intentionally as it is never bound, so the connection is always refused. +"!godot_binary!" --path . -s -d --remote-debug tcp://127.0.0.1:0 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 100755 index 0000000..3504131 --- /dev/null +++ b/addons/gdUnit4/runtest.sh @@ -0,0 +1,65 @@ +#!/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+=("$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. +# --remote-debug tcp://127.0.0.1:0 prevents Godot from activating its local interactive +# CLI debugger, which would cause an endless 'debug>' loop on script parse errors. +# Port 0 is used intentionally as it is never bound, so the connection is always refused. +"$godot_binary" --path . -s -d --remote-debug tcp://127.0.0.1:0 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..f112356 --- /dev/null +++ b/addons/gdUnit4/src/Comparator.gd.uid @@ -0,0 +1 @@ +uid://dxnkski2eyiqq diff --git a/addons/gdUnit4/src/Fuzzers.gd b/addons/gdUnit4/src/Fuzzers.gd new file mode 100644 index 0000000..c8689df --- /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 0000000..9160200 --- /dev/null +++ b/addons/gdUnit4/src/Fuzzers.gd.uid @@ -0,0 +1 @@ +uid://swhmyrrfswfv 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..c077fbf --- /dev/null +++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid @@ -0,0 +1 @@ +uid://cwm3hrvqhodyd 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..40b96fe --- /dev/null +++ b/addons/gdUnit4/src/GdUnitAssert.gd.uid @@ -0,0 +1 @@ +uid://cnouujwdbf60c diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd new file mode 100644 index 0000000..06e3171 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd @@ -0,0 +1,73 @@ +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 stack_trace := GdUnitStackTrace.new() + var line_number := stack_trace.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 := GdUnitStackTrace.new().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 0000000..ec22c54 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd.uid @@ -0,0 +1 @@ +uid://c34bgux3dwr7p diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd b/addons/gdUnit4/src/GdUnitBoolAssert.gd new file mode 100644 index 0000000..714f8fc --- /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 0000000..fa92f1a --- /dev/null +++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid @@ -0,0 +1 @@ +uid://di05ej5qc6a1e diff --git a/addons/gdUnit4/src/GdUnitConstants.gd b/addons/gdUnit4/src/GdUnitConstants.gd new file mode 100644 index 0000000..e43c75a --- /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 0000000..7e6e02b --- /dev/null +++ b/addons/gdUnit4/src/GdUnitConstants.gd.uid @@ -0,0 +1 @@ +uid://2atd8h0dym3h diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd new file mode 100644 index 0000000..45cc62a --- /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 0000000..3241df5 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid @@ -0,0 +1 @@ +uid://dryx8l4tqyxl5 diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd b/addons/gdUnit4/src/GdUnitFailureAssert.gd new file mode 100644 index 0000000..de34aa8 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd @@ -0,0 +1,56 @@ +## 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 stack trace is equal to expected one. +@abstract func has_stack_trace(stack_trace: Array[GdUnitStackTraceElement]) -> 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 0000000..9be76a4 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid @@ -0,0 +1 @@ +uid://dysw4ml0wxsle diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd b/addons/gdUnit4/src/GdUnitFileAssert.gd new file mode 100644 index 0000000..771da90 --- /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 0000000..a2e3f9a --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFileAssert.gd.uid @@ -0,0 +1 @@ +uid://4e5acfclao84 diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd b/addons/gdUnit4/src/GdUnitFloatAssert.gd new file mode 100644 index 0000000..2695ab0 --- /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 0000000..bfedfaf --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid @@ -0,0 +1 @@ +uid://jqo3qkbhssg2 diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd b/addons/gdUnit4/src/GdUnitFuncAssert.gd new file mode 100644 index 0000000..e8a49c5 --- /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 0000000..9452261 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid @@ -0,0 +1 @@ +uid://bpx5yxsto6qsk diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd new file mode 100644 index 0000000..01711f9 --- /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 0000000..043c484 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid @@ -0,0 +1 @@ +uid://w6yrurb5ccqk diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd b/addons/gdUnit4/src/GdUnitIntAssert.gd new file mode 100644 index 0000000..05eb922 --- /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 0000000..eb3ea0c --- /dev/null +++ b/addons/gdUnit4/src/GdUnitIntAssert.gd.uid @@ -0,0 +1 @@ +uid://de60xydv2li4s diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd b/addons/gdUnit4/src/GdUnitObjectAssert.gd new file mode 100644 index 0000000..9d7e76e --- /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 0000000..ede572a --- /dev/null +++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid @@ -0,0 +1 @@ +uid://xmvq25x8w05j diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd b/addons/gdUnit4/src/GdUnitResultAssert.gd new file mode 100644 index 0000000..01eb880 --- /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 0000000..4db263f --- /dev/null +++ b/addons/gdUnit4/src/GdUnitResultAssert.gd.uid @@ -0,0 +1 @@ +uid://w2de5nu7l1u2 diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd b/addons/gdUnit4/src/GdUnitSceneRunner.gd new file mode 100644 index 0000000..9b156f3 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd @@ -0,0 +1,350 @@ +## 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] +## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0 +## [member key_code] : the key code e.g. [constant KEY_ENTER][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 pressing.[br] +## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0[br]See `test_key_shift_and_A_presssing` for example using key combinations +## [member key_code] : the key code e.g. [constant KEY_ENTER][br] +## [codeblock] +## # Do simulate key pressing A +## func test_key_A_presssing(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_key_press(KEY_A) +## +## +## # Do simulate keycombination pressing shift+A +## func test_key_shift_and_A_presssing(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## runner.simulate_key_press(KEY_SHIFT) +## runner.simulate_key_press(KEY_A) +## await _runner.await_input_processed() +## [/codeblock] +@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] +## [codeblock] +## # Do simulate releasing key A +## func test_key_A_releasing(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_key_release(KEY_A) +## +## +## # Do simulate keycombination pressing shift+A +## func test_key_shift_and_A_releasing((): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## runner.simulate_key_release(KEY_SHIFT) +## runner.simulate_key_release(KEY_A) +## await _runner.await_input_processed() +## [/codeblock] +## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0[br]See `test_key_shift_and_A_releasing` for example using key combinations +@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 0000000..c8f08bd --- /dev/null +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid @@ -0,0 +1 @@ +uid://oee7g5i83hut diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd b/addons/gdUnit4/src/GdUnitSignalAssert.gd new file mode 100644 index 0000000..2c3d39b --- /dev/null +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd @@ -0,0 +1,158 @@ +## 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 the specified signal is emitted with the expected arguments.[br] +## +## This assertion waits for a signal to be emitted from the object under test and +## validates that it was emitted with the correct arguments. The function supports +## both typed signals (Signal type) and string-based signal names for flexibility +## in different testing scenarios.[br] +## [br] +## [b]Parameters:[/b][br] +## [param signal_name]: The signal to monitor. Can be either:[br] +## โ€ข A [Signal] reference (recommended for type safety)[br] +## โ€ข A [String] with the signal name +## [param signal_args]: Optional expected signal arguments.[br] +## When provided, verifies the signal was emitted with exactly these values.[br] +## [br] +## [b]Returns:[/b][br] +## [GdUnitSignalAssert] - Returns self for method chaining.[br] +## [br] +## [b]Examples:[/b] +## [codeblock] +## signal signal_a(value: int) +## signal signal_b(name: String, count: int) +## +## # Wait for signal emission without checking arguments +## # Using Signal reference (type-safe) +## await assert_signal(instance).is_emitted(signal_a) +## # Using string name (dynamic) +## await assert_signal(instance).is_emitted("signal_a") +## +## # Wait for signal emission with specific argument +## await assert_signal(instance).is_emitted(signal_a, 10) +## +## # Wait for signal with multiple arguments +## await assert_signal(instance).is_emitted(signal_b, "test", 42) +## +## # Wait max 500ms for signal with argument 10 +## await assert_signal(instance).wait_until(500).is_emitted(signal_a, 10) +## [/codeblock] +## [br] +## [b]Note:[/b] This is an async operation - use [code]await[/code] when calling.[br] +## The assertion fails if the signal is not emitted within the timeout period. +@abstract func is_emitted(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert + + +## Verifies that the specified signal is NOT emitted with the expected arguments.[br] +## +## This assertion waits for a specified time period and validates that a signal +## was not emitted with the given arguments. Useful for ensuring certain conditions +## don't trigger unwanted signals or for verifying signal filtering logic.[br] +## [br] +## [b]Parameters:[/b][br] +## [param signal_name]: The signal to monitor. Can be either:[br] +## โ€ข A [Signal] reference (recommended for type safety)[br] +## โ€ข A [String] with the signal name +## [param signal_args]: Optional expected signal arguments.[br] +## When provided, verifies the signal was not emitted with these specific values.[br] +## If omitted, verifies the signal was not emitted at all.[br] +## [br] +## [b]Returns:[/b][br] +## [GdUnitSignalAssert] - Returns self for method chaining.[br] +## [br] +## [b]Examples:[/b] +## [codeblock] +## signal signal_a(value: int) +## signal signal_b(name: String, count: int) +## +## # Verify signal is not emitted at all (without checking arguments) +## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a) +## await assert_signal(instance).wait_until(500).is_not_emitted("signal_a") +## +## # Verify signal is not emitted with specific argument +## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a, 10) +## +## # Verify signal is not emitted with multiple arguments +## await assert_signal(instance).wait_until(500).is_not_emitted(signal_b, "test", 42) +## +## # Can be emitted with different arguments (this passes) +## instance.emit_signal("signal_a", 20) # Emits with 20, not 10 +## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a, 10) +## [/codeblock] +## [br] +## [b]Note:[/b] This is an async operation - use [code]await[/code] when calling.[br] +## The assertion fails if the signal IS emitted with the specified arguments within the timeout period. +@abstract func is_not_emitted(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert + + +## Verifies that the specified signal exists on the emitter object.[br] +## +## This assertion checks if a signal is defined on the object under test, +## regardless of whether it has been emitted. Useful for validating that +## objects have the expected signals before testing their emission.[br] +## [br] +## [b]Parameters:[/b][br] +## [param signal_name]: The signal to check. Can be either:[br] +## โ€ข A [Signal] reference (recommended for type safety)[br] +## โ€ข A [String] with the signal name +## [br] +## [b]Returns:[/b][br] +## [GdUnitSignalAssert] - Returns self for method chaining.[br] +## [br] +## [b]Examples:[/b] +## [codeblock] +## signal my_signal(value: int) +## signal another_signal() +## +## # Verify signal exists using Signal reference +## assert_signal(instance).is_signal_exists(my_signal) +## +## # Verify signal exists using string name +## assert_signal(instance).is_signal_exists("my_signal") +## +## # Chain with other assertions +## assert_signal(instance) \ +## .is_signal_exists(my_signal) \ +## .is_emitted(my_signal, 42) +## +## [/codeblock] +## [br] +## [b]Note:[/b] This only checks signal definition, not emission.[br] +## The assertion fails if the signal is not defined on the object. +@abstract func is_signal_exists(signal_name: Variant) -> GdUnitSignalAssert + + +## Sets the assert signal timeout in ms, if the time over a failure is reported.[br] +## Example: +## [codeblock] +## do wait until 5s the instance has emitted the signal `signal_a`[br] +## assert_signal(instance).wait_until(5000).is_emitted("signal_a") +## [/codeblock] +@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 0000000..51b7e7e --- /dev/null +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid @@ -0,0 +1 @@ +uid://d4hpogpxwv8nb diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd b/addons/gdUnit4/src/GdUnitStringAssert.gd new file mode 100644 index 0000000..2de698b --- /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 0000000..801982b --- /dev/null +++ b/addons/gdUnit4/src/GdUnitStringAssert.gd.uid @@ -0,0 +1 @@ +uid://j3bjfrair82i diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd b/addons/gdUnit4/src/GdUnitTestSuite.gd new file mode 100644 index 0000000..0d58340 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd @@ -0,0 +1,746 @@ +## 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 + + +### 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 := "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 +static 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() -> 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) + + +## Collects detailed information about orphaned nodes for debugging purposes.[br] +## +## This function gathers comprehensive details about nodes that remain in memory +## after test execution (orphans). It provides debugging information to help +## identify the source of memory leaks in tests. Must be manually called in +## tests when orphan nodes are detected.[br] +## [br] +## [b]When to Use:[/b][br] +## - When GdUnit4 reports orphan nodes after test execution[br] +## - For debugging memory leaks in test scenarios[br] +## - To get detailed information about unreleased nodes[br] +## [br] +## [b]Usage Pattern:[/b][br] +## Add this call at the end of tests that are suspected to create orphans, +## or when the test runner reports orphan detection.[br] +## [br] +## [b]Examples:[/b] +## [codeblock] +## func test_scene_management(): +## # Test code that might create orphan nodes +## var scene = preload("res://TestScene.tscn").instantiate() +## add_child(scene) +## +## # Do test operations +## scene.some_method() +## +## # Clean up (but might miss some nodes) +## scene.queue_free() +## +## # Collect orphan details if any are detected +## collect_orphan_node_details() +## [/codeblock] +## [br] +## [b]Note:[/b] This is a debugging utility function that should be removed +## or commented out once orphan issues are resolved. +func collect_orphan_node_details() -> void: + GdUnitThreadManager.get_current_context().get_execution_context().orphan_monitor_collect() + + +## 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) + + +## Creates a GdUnitTuple from the provided arguments for use in test assertions. +## [br] +## This is the primary helper function for creating tuples in GdUnit4 tests. +## It provides a convenient way to group multiple expected values when using +## [method extractv] assertions. The function enforces that tuples must contain +## at least two values, as single-value extractions don't require tuple grouping. +## [br] +## [b]Parameters:[/b] [br] +## - [code]...args[/code]: Variable number of arguments (minimum 2) to group into a tuple. +## Each argument represents a value to be compared in assertions. +## [br] +## [b]Returns:[/b] [br] +## A [GdUnitTuple] containing the provided values, or an empty tuple if fewer than +## 2 arguments are provided (with an error message). +## [br] +## [b]Error Handling:[/b] [br] +## [codeblock] +## # This will push an error and return empty tuple +## var invalid = tuple("single_value") # Error: requires at least 2 arguments +## [br] +## # Correct usage - minimum 2 arguments +## var valid = tuple("name", "value") +## var valid_multi = tuple(1, 2, 3, 4, 5) # Can have many values +## [/codeblock] +func tuple(...args: Array) -> GdUnitTuple: + if args.size() < 2: + push_error("Tuple requires at least two arguments.") + return GdUnitTuple.new() + return GdUnitTuple.new.callv(args) + + +# === 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. +static 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 0000000..0c19f21 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd.uid @@ -0,0 +1 @@ +uid://dabha2js2bw8d diff --git a/addons/gdUnit4/src/GdUnitTuple.gd b/addons/gdUnit4/src/GdUnitTuple.gd new file mode 100644 index 0000000..9d56dd8 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitTuple.gd @@ -0,0 +1,86 @@ +## A tuple implementation for GdUnit4 test assertions and value extraction. +## @tutorial(GdUnit4 Array Assertions): https://mikeschulze.github.io/gdUnit4/latest/testing/assert-array/#extractv +## @tutorial(GdUnit4 Testing Framework): https://mikeschulze.github.io/gdUnit4/ +## [br] +## The GdUnitTuple class is a utility container designed specifically for the GdUnit4 +## testing framework. It enables advanced assertion operations, particularly when +## extracting and comparing multiple values from complex test results. +## [br] +## [b]Primary Use Cases in Testing:[/b] [br] +## - Extracting multiple properties from test objects with [method extractv]## [br] +## - Grouping related assertion values for comparison## [br] +## - Returning multiple values from test helper methods## [br] +## - Organizing expected vs actual value pairs in assertions## [br] +## [br] +## [b]Example Usage in Tests:[/b] +## [codeblock] +## func test_player_stats_after_level_up(): +## var player = Player.new() +## player.level_up() +## +## # Extract multiple properties using tuple +## assert_array([player]) \ +## .extractv(extr("name"), extr("level"), extr("hp")) \ +## .contains(tuple("Hero", 2, 150)) +## +## func test_enemy_spawn_positions(): +## var enemies: Array = spawn_enemies(3) +## +## # Verify multiple enemies have correct position data +## assert_array(enemies) \ +## .extractv(extr("position.x"), extr("position.y")) \ +## .contains_exactly([ +## tuple(100, 200), +## tuple(150, 200), +## tuple(200, 200) +## ]) +## [/codeblock] +## [br] +## [b]Integration with GdUnit4 Assertions:[/b] [br] +## Tuples work seamlessly with array assertion methods like: [br] +## - [code]contains()[/code] - Check if extracted values contain specific tuples [br] +## - [code]contains_exactly()[/code] - Verify exact tuple matches [br] +## - [code]is_equal()[/code] - Compare tuple equality [br] +## [br] +## [b]Note:[/b] This class is part of the GdUnit4 testing framework's internal +## utilities and is primarily intended for use within test assertions rather +## than production code. +class_name GdUnitTuple +extends RefCounted + +var _values: Array = [] + + +## Initializes a new GdUnitTuple with test values. +## [br] +## Creates a tuple to hold multiple values extracted from test objects +## or expected values for assertions. Commonly used with the [code]tuple()[/code] +## helper function in GdUnit4 tests. +## [br] +## [b]Parameters:[/b] +## - [code]...args[/code]: Variable number of values to store. +func _init(...args: Array) -> void: + _values = args + + +## Returns the tuple's values as an array for assertion comparisons. +## [br] +## Provides access to the stored test values. Used internally by GdUnit4's +## assertion system when comparing tuples in test validations. +## [br] +## [b]Returns:[/b] +## An [Array] containing all values stored in the tuple. +func values() -> Array: + return _values + + +## Returns a string representation for test output and debugging. +## [br] +## Formats the tuple for display in test results, error messages, and debug logs. +## This method is automatically called by GdUnit4 when displaying assertion +## failures involving tuples. +## [br] +## [b]Returns:[/b] +## A [String] in the format "tuple([value1, value2, ...])" +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 0000000..a50bdb9 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitTuple.gd.uid @@ -0,0 +1 @@ +uid://gxd48fh4qk28 diff --git a/addons/gdUnit4/src/GdUnitValueExtractor.gd b/addons/gdUnit4/src/GdUnitValueExtractor.gd new file mode 100644 index 0000000..1a34445 --- /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 0000000..18a9d96 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid @@ -0,0 +1 @@ +uid://d4b4f5x4jins7 diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd b/addons/gdUnit4/src/GdUnitVectorAssert.gd new file mode 100644 index 0000000..c186cba --- /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 0000000..1510d2c --- /dev/null +++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid @@ -0,0 +1 @@ +uid://clpmgumtclvdp diff --git a/addons/gdUnit4/src/asserts/CLAUDE.md b/addons/gdUnit4/src/asserts/CLAUDE.md new file mode 100644 index 0000000..b7ccbb3 --- /dev/null +++ b/addons/gdUnit4/src/asserts/CLAUDE.md @@ -0,0 +1,137 @@ +# Implementing a New Assert Type + +Every assert type follows a strict two-layer pattern. Five touch points must be updated โ€” do them in this order. + +## 1. Abstract interface โ€” `src/GdUnitAssert.gd` + +Read `src/GdUnitAssert.gd` to get the exact base interface โ€” re-declare every `@abstract` method +from it with the return type changed to `GdUnitAssert`, then append type-specific methods below. +Do **not** hardcode the method list; derive it from the current file. + +## 2. Implementation โ€” `src/asserts/GdUnitAssertImpl.gd` + +All logic delegates to a `GdUnitAssertImpl` instance. The mandatory skeleton: + +```gdscript +extends GdUnitAssert + +var _base: GdUnitAssertImpl + +func _init(current: Variant) -> void: + _base = GdUnitAssertImpl.new(current) + GdUnitThreadManager.get_current_context().set_assert(self) + if not _validate_value_type(current): + @warning_ignore("return_value_discarded") + report_error("GdUnitAssert error, the type <%s> is not supported." % GdObjects.typeof_as_string(current)) + +func _notification(event: int) -> void: # required โ€” cleans up _base + 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 MyGodotType + +func current_value() -> Variant: + return _base.current_value() + +func report_success() -> GdUnitAssert: + @warning_ignore("return_value_discarded") + _base.report_success() + return self + +func report_error(error: String) -> GdUnitAssert: + @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) -> GdUnitAssert: + @warning_ignore("return_value_discarded") + _base.override_failure_message(message) + return self + +func append_failure_message(message: String) -> GdUnitAssert: + @warning_ignore("return_value_discarded") + _base.append_failure_message(message) + return self + +func is_null() -> GdUnitAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + +func is_not_null() -> GdUnitAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + +func is_equal(expected: Variant) -> GdUnitAssert: + @warning_ignore("return_value_discarded") + _base.is_equal(expected) # or custom comparison + report_error/report_success + return self + +func is_not_equal(expected: Variant) -> GdUnitAssert: + @warning_ignore("return_value_discarded") + _base.is_not_equal(expected) + return self +``` + +Key rules: + +- Every method returns `self` for chaining. +- Call `report_error(message)` on failure, `report_success()` on pass โ€” never raise directly. +- `GdObjects.equals()` handles deep comparison for most types. For objects whose `==` is + reference-only (e.g. `Image`), implement comparison manually via their API. +- Custom error message helpers belong in `GdAssertMessages.gd` as `static func` methods. + Use `_error()` / `_colored_value()` for consistent colouring. + +## 3. Preload โ€” `src/asserts/GdUnitAssertions.gd` + +Add one line inside `_init()` alongside the other preloads: + +```gdscript +GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") +``` + +## 4. Registration โ€” `src/GdUnitTestSuite.gd` + +Add the public factory method near the other `assert_*` functions: + +```gdscript +func assert_mytype(current: Variant) -> GdUnitAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd").new(current) +``` + +Update `assert_that()` to dispatch the new type (add a `match` branch or an `if` inside `TYPE_OBJECT`): + +```gdscript +TYPE_OBJECT: + if current is MyGodotType: + return assert_mytype(current) + return assert_object(current) +``` + +## 5. Test suite โ€” `test/asserts/GdUnitAssertImplTest.gd` + +```gdscript +class_name GdUnitAssertImplTest +extends GdUnitTestSuite + + +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd' +``` + +Minimum test coverage: supported types, unsupported types (type-check error message), +`is_null`, `is_not_null`, `is_equal` (pass + each failure variant), `is_not_equal`, +every type-specific method, `assert_that` dispatch, method chaining. + +**Lint scope:** `test/asserts/` is **not** checked by CI gdlint. Only `src/asserts/` is. +Always lint `src/asserts/` after changes: + +```bash +gdlint addons/gdUnit4/src/asserts/ +``` diff --git a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd new file mode 100644 index 0000000..6be4b3e --- /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 0000000..bc3046d --- /dev/null +++ b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid @@ -0,0 +1 @@ +uid://bp7n72kpewtv4 diff --git a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd new file mode 100644 index 0000000..2f828fa --- /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 0000000..e5fac5f --- /dev/null +++ b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid @@ -0,0 +1 @@ +uid://c783yy5i2141l diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd new file mode 100644 index 0000000..9174bc4 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd @@ -0,0 +1,757 @@ +class_name GdAssertMessages +extends Resource + + +const SUB_COLOR := Color(1, 0, 0, .15) +const ADD_COLOR := Color(0, 1, 0, .15) + +const NO_ORPHAN_DETAILS := "โš ๏ธNo details available. Run tests in debug mode to collect details." + +# 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 var _warn_color := GdUnitEditorColorTheme.state_warning.to_html() +static var _error_color := GdUnitEditorColorTheme.state_failure.to_html() +static var _value_color := GdUnitEditorColorTheme.value_color.to_html() + + +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 : keycode=%s (%s) pressed: %s + physical_keycode: %s + location: %s + echo: %s""" % [ + key_event.keycode, + key_event.as_text_keycode(), + key_event.pressed, + key_event.physical_keycode, + key_event.location, + key_event.echo] + 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 += """ + -------- + mods: %s + shift: %s + alt: %s + control: %s + meta: %s + command: %s""" % [ + mouse_event.get_modifiers_mask(), + mouse_event.shift_pressed, + mouse_event.alt_pressed, + mouse_event.ctrl_pressed, + mouse_event.meta_pressed, + mouse_event.command_or_control_autoremap] + return text.dedent() + + +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: Variant, color: Color) -> String: + return "[color=%s]%s[/color]" % [color.to_html(), value] + + +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_warning(orphans_count: int) -> String: + if EngineDebugger.is_active(): + return """ + %s Detected %s possible orphan nodes. + To collect detailed information, insert this block at the end of your test. + %s""".dedent().trim_prefix("\n") % [ + _warning("WARNING:"), + _nerror(orphans_count), + _colored(""" + [code] + await get_tree().process_frame + collect_orphan_node_details() + [/code]""".dedent().trim_prefix("\n"), GdUnitEditorColorTheme.value_color) + ] + return """ + %s Detected %s possible orphan nodes. + %s + """.dedent().trim_prefix("\n") % [ + _warning("WARNING:"), + _nerror(orphans_count), + _warning(NO_ORPHAN_DETAILS) + ] + + +static func orphan_detected(orphan_count: int) -> String: + if EngineDebugger.is_active(): + return """ + %s Detected %s orphan nodes.""".dedent().trim_prefix("\n") % [ + _warning("WARNING:"), + _nerror(orphan_count) + ] + return """ + %s Detected %s orphan nodes. + %s""".dedent().trim_prefix("\n") % [ + _warning("WARNING:"), + _nerror(orphan_count), + _warning(NO_ORPHAN_DETAILS) + ] + + +static func orphan_node_info(orphan_info: GdUnitOrphanNodeInfo) -> String: + var message := "<%s> Id:%s" % [ + _colored(orphan_info._type, GdUnitEditorColorTheme.engine_type_color), + _colored(orphan_info._id, GdUnitEditorColorTheme.engine_type_color) + ] + if orphan_info._stack_element == null: + message += _warning("\n\tNo source info available") + return message + + +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))] + + +static func test_session_terminated() -> String: + return "%s" % _error("Test Session Terminated") + + +# 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 0000000..c59de53 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid @@ -0,0 +1 @@ +uid://cxexirfo6npk4 diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd b/addons/gdUnit4/src/asserts/GdAssertReports.gd new file mode 100644 index 0000000..11c85db --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd @@ -0,0 +1,57 @@ +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(error: GdUnitError) -> void: + GdUnitSignals.instance().gdunit_set_test_failed.emit(true) + GdAssertReports.set_last_error_line_number(error._line_number) + Engine.set_meta(LAST_ERROR, error._message) + Engine.set_meta("GD_TEST_FAILURE", true) + GdUnitThreadManager.get_current_context().get_execution_context().set_error(error) + + # if we expect to fail we handle as success test + if _do_expect_assert_failing(): + return + send_report(GdUnitReport.new().from_error(GdUnitReport.FAILURE, error)) + + +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 0000000..8eb7aa5 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid @@ -0,0 +1 @@ +uid://dfhdnqtqepnyx diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd new file mode 100644 index 0000000..f659b96 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd @@ -0,0 +1,420 @@ +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 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: + 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] = [] + for index: int in extractors.size(): + var extractor: GdUnitValueExtractor = extractors[index] + ev.append(extractor.extract_value(element)) + if extractors.size() > 1: + extracted_elements.append(GdUnitTuple.new.callv(ev)) + 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 0000000..6f68959 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cgmyovosuf68a diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd new file mode 100644 index 0000000..f904595 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd @@ -0,0 +1,74 @@ +class_name GdUnitAssertImpl +extends GdUnitAssert + + +var _current :Variant +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 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 stack_trace := GdUnitStackTrace.new() + var line_number := failure_line_number if failure_line_number != -1 else stack_trace.get_line_number() + GdAssertReports.set_last_error_line_number(line_number) + var failure_message := GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(GdUnitError.new(failure_message, line_number, stack_trace)) + 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 0000000..0c2daa9 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://ca3jhjdy5rg1f diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd new file mode 100644 index 0000000..2c4bb1a --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd @@ -0,0 +1,39 @@ +# 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 diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid new file mode 100644 index 0000000..f71a001 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid @@ -0,0 +1 @@ +uid://op4pwxjtrxh7 diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd new file mode 100644 index 0000000..e2c0c27 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd @@ -0,0 +1,83 @@ +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 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 0000000..307257c --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://ccccmqlltkyxt diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd new file mode 100644 index 0000000..175c161 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd @@ -0,0 +1,203 @@ +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 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 0000000..72644db --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://bxw43pkll5j16 diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd new file mode 100644 index 0000000..2fb195b --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd @@ -0,0 +1,149 @@ +extends GdUnitFailureAssert + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _is_failed := false +var _failure_message := "" +var _stack_trace: GdUnitStackTrace +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 + + var last_error := thread_context.get_execution_context().last_error() + if last_error != null: + _stack_trace = last_error._stack_trace + _failure_message = last_error._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_stack_trace(stack_trace_elements: Array[GdUnitStackTraceElement]) -> GdUnitFailureAssert: + var current_stack_trace := str(_stack_trace) + var expected_stack_trace := str(GdUnitStackTrace.of(stack_trace_elements)) + if current_stack_trace != expected_stack_trace: + return _report_error(GdAssertMessages.error_equal(current_stack_trace, expected_stack_trace)) + 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 stack_trace := GdUnitStackTrace.new() + var line_number := failure_line_number if failure_line_number != -1 else stack_trace.get_line_number() + var failure_message := GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(GdUnitError.new(failure_message, line_number, stack_trace)) + 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 0000000..50de41b --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://bfq4kfm0wpm4b diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd new file mode 100644 index 0000000..7df9985 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd @@ -0,0 +1,112 @@ +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 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 0000000..f421609 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://dlihl26awlp0j diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd new file mode 100644 index 0000000..a1b422e --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd @@ -0,0 +1,155 @@ +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 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 0000000..2a5ed77 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://c7qe0r4dtthtq diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd new file mode 100644 index 0000000..0df7389 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd @@ -0,0 +1,172 @@ +extends GdUnitFuncAssert + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const DEFAULT_TIMEOUT := 2000 + + +var _current_value_provider :ValueProvider +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 = GdUnitStackTrace.new().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: + var failure_message := GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(GdUnitError.new(failure_message, _line_number, GdUnitStackTrace.new())) + return self + + +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 0000000..2abef26 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cqvaa8tqvfuuf diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd new file mode 100644 index 0000000..a868987 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd @@ -0,0 +1,147 @@ +extends GdUnitGodotErrorAssert + +var _custom_failure_message := "" +var _additional_failure_message := "" +var _callable: Callable +var _logger := GodotGdErrorMonitor.GdUnitLogger.new(true, true) + + +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]: + await _callable.call() + return _logger.entries() + + +func _report_success() -> GdUnitAssert: + GdAssertReports.report_success() + return self + + +func _report_error(error_message: String, failure_line_number: int = -1) -> GdUnitAssert: + var stack_trace := GdUnitStackTrace.new() + var line_number := failure_line_number if failure_line_number != -1 else stack_trace.get_line_number() + var failure_message := GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(GdUnitError.new(failure_message, line_number, stack_trace)) + 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): + GdUnitThreadManager.get_current_context().get_execution_context().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" + + var values := [] + for entry in log_entries: + values.append(entry) + return "\n".join(values) + + +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: + if not _validate_callable(): + return self + + 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: + if not _validate_callable(): + return self + + 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. + expected: '%s' + current: '%s' + """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)]) + + +func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert: + if not _validate_callable(): + return self + + 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. + expected: '%s' + current: '%s' + """.dedent().trim_prefix("\n") % [expected_warning, _to_list(log_entries)]) + + +func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert: + if not _validate_callable(): + return self + + 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. + expected: '%s' + current: '%s' + """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)]) + + +func _validate_callable() -> bool: + if _callable == null or not _callable.is_valid(): + @warning_ignore("return_value_discarded") + _report_error("Invalid Callable '%s'" % _callable) + return false + return true diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid new file mode 100644 index 0000000..f1671cc --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cf0nm5vtd0wes diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd new file mode 100644 index 0000000..7f17e86 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd @@ -0,0 +1,162 @@ +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 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 0000000..7c9da87 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://cbbj2pku5n13 diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd new file mode 100644 index 0000000..9816662 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd @@ -0,0 +1,162 @@ +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 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 0000000..0a2c1b9 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://dh5wovuq2j8t6 diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd new file mode 100644 index 0000000..b45c46d --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -0,0 +1,124 @@ +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 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 0000000..aad49ce --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://d0dee1aef1iwi diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd new file mode 100644 index 0000000..99ca544 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd @@ -0,0 +1,167 @@ +extends GdUnitSignalAssert + +const DEFAULT_TIMEOUT := 2000 + +var _signal_collector :GdUnitSignalCollector +var _emitter :Object +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 = GdUnitStackTrace.new().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, GdUnitStackTrace.new().get_line_number()) + return self + + +func report_error(failure :String) -> GdUnitAssert: + var failure_message := GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(GdUnitError.new(failure_message, _line_number, GdUnitStackTrace.new())) + return self + + +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_or_name: Variant) -> GdUnitSignalAssert: + if not (signal_or_name is String or signal_or_name is Signal): + return report_error("Invalid signal_name: expected String or Signal, but is '%s'" % type_string(typeof(signal_or_name))) + + var signal_name := _to_signal_name(signal_or_name) + + 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(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert: + _line_number = GdUnitStackTrace.new().get_line_number() + @warning_ignore("unsafe_call_argument") + return await _wail_until_signal( + signal_name, + _wrap_arguments.callv(signal_args), + false) + + +# Verifies that given signal is NOT emitted until waiting time +func is_not_emitted(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert: + _line_number = GdUnitStackTrace.new().get_line_number() + @warning_ignore("unsafe_call_argument") + return await _wail_until_signal( + signal_name, + _wrap_arguments.callv(signal_args), + true) + + +func _wrap_arguments(...args: Array) -> Array: + # Check using old syntax + if not args.is_empty() and args[0] is Array: + return args[0] + return args + + +func _wail_until_signal(signal_or_name: Variant, expected_args: Array, expect_not_emitted: bool) -> GdUnitSignalAssert: + if _emitter == null: + return report_error("Can't wait for signal checked a NULL object.") + if not (signal_or_name is String or signal_or_name is Signal): + return report_error("Invalid signal_name: expected String or Signal, but is '%s'" % type_string(typeof(signal_or_name))) + + var signal_name := _to_signal_name(signal_or_name) + # 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 + + +func _to_signal_name(signal_or_name: Variant) -> String: + @warning_ignore("unsafe_cast") + return (signal_or_name as Signal).get_name() if signal_or_name is Signal else signal_or_name diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid new file mode 100644 index 0000000..e197aca --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://b02dgi42k4ue2 diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd new file mode 100644 index 0000000..54d613e --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd @@ -0,0 +1,204 @@ +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 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 0000000..45fa8bf --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://blq43fhotrwnk diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd new file mode 100644 index 0000000..2a53349 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd @@ -0,0 +1,183 @@ +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 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 0000000..3ea8332 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid @@ -0,0 +1 @@ +uid://iw6kt4ajtw7w diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd b/addons/gdUnit4/src/asserts/ValueProvider.gd new file mode 100644 index 0000000..be01f70 --- /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 0000000..f39b773 --- /dev/null +++ b/addons/gdUnit4/src/asserts/ValueProvider.gd.uid @@ -0,0 +1 @@ +uid://d07y8ue0ocu0p diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd new file mode 100644 index 0000000..aa02319 --- /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 0000000..117c0df --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid @@ -0,0 +1 @@ +uid://dwf6wnercsm0u diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd b/addons/gdUnit4/src/cmd/CmdCommand.gd new file mode 100644 index 0000000..92e8c1f --- /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 0000000..2a6fd56 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdCommand.gd.uid @@ -0,0 +1 @@ +uid://b7purph8acgat diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd new file mode 100644 index 0000000..a60719f --- /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 self + + 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 self + + 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 0000000..e98a1b7 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid @@ -0,0 +1 @@ +uid://boulonef3r0j diff --git a/addons/gdUnit4/src/cmd/CmdOption.gd b/addons/gdUnit4/src/cmd/CmdOption.gd new file mode 100644 index 0000000..a4982de --- /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 0000000..91eb039 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdOption.gd.uid @@ -0,0 +1 @@ +uid://bupv03t106ita diff --git a/addons/gdUnit4/src/cmd/CmdOptions.gd b/addons/gdUnit4/src/cmd/CmdOptions.gd new file mode 100644 index 0000000..c610529 --- /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 0000000..5d86f36 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdOptions.gd.uid @@ -0,0 +1 @@ +uid://ctvcvfanurvqe diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd b/addons/gdUnit4/src/core/GdArrayTools.gd new file mode 100644 index 0000000..74f0e17 --- /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 0000000..b155504 --- /dev/null +++ b/addons/gdUnit4/src/core/GdArrayTools.gd.uid @@ -0,0 +1 @@ +uid://bobdkgif6d42o diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd b/addons/gdUnit4/src/core/GdDiffTool.gd new file mode 100644 index 0000000..ad36470 --- /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 + elif (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() + + elif text1Words[i] != lcsList[k]: + while i < text1Words.size() and text1Words[i] != lcsList[k]: + stringBuffer += "" + text1Words[i] + " " + i += 1 + elif 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 0000000..0ddee71 --- /dev/null +++ b/addons/gdUnit4/src/core/GdDiffTool.gd.uid @@ -0,0 +1 @@ +uid://iqqjoo4po56k diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd new file mode 100644 index 0000000..279e518 --- /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 0000000..a5865d4 --- /dev/null +++ b/addons/gdUnit4/src/core/GdObjects.gd.uid @@ -0,0 +1 @@ +uid://bgyyqtnjytxub diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd b/addons/gdUnit4/src/core/GdUnit4Version.gd new file mode 100644 index 0000000..3e6a334 --- /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 0000000..1633611 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnit4Version.gd.uid @@ -0,0 +1 @@ +uid://br0sukav0ayo1 diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd b/addons/gdUnit4/src/core/GdUnitFileAccess.gd new file mode 100644 index 0000000..d4f1449 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd @@ -0,0 +1,235 @@ +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() + + +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) + if file == null: + zip.close() + return GdUnitResult.error("Can't open file to write: '%s'. Error: %s" % [new_file_path, error_string(FileAccess.get_open_error())]) + 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 0000000..9806ebb --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid @@ -0,0 +1 @@ +uid://bf2sah1gcbu18 diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd b/addons/gdUnit4/src/core/GdUnitProperty.gd new file mode 100644 index 0000000..3d050b1 --- /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 0000000..7782d12 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitProperty.gd.uid @@ -0,0 +1 @@ +uid://ct1ccf3vax8bu diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd b/addons/gdUnit4/src/core/GdUnitResult.gd new file mode 100644 index 0000000..42392a5 --- /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 0000000..d0c1711 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitResult.gd.uid @@ -0,0 +1 @@ +uid://ciwq04mimce7w diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd new file mode 100644 index 0000000..0d1a42d --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -0,0 +1,139 @@ +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, + + # Exit on first failure + EXIT_FAIL_FAST : false + } + + +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 do_fail_fast(fail_fast: bool) -> GdUnitRunnerConfig: + _config[EXIT_FAIL_FAST] = fail_fast + return self + + +func is_fail_fast() -> bool: + return _config.get(EXIT_FAIL_FAST, false) + + +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, + EXIT_FAIL_FAST : is_fail_fast(), + SERVER_PORT : 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 0000000..00a2187 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid @@ -0,0 +1 @@ +uid://jb1j7d4auiip diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd new file mode 100644 index 0000000..052585b --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -0,0 +1,645 @@ +# 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) + Engine.set_meta("GdUnitSceneRunner", self) + # 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: + _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 + Engine.remove_meta("GdUnitSceneRunner") + + +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: + _push_warning_deprecated_arguments(shift_pressed, ctrl_pressed) + 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: + _push_warning_deprecated_arguments(shift_pressed, ctrl_pressed) + __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.set_alt_pressed(key_code == KEY_ALT) + event.set_shift_pressed(shift_pressed) + event.set_ctrl_pressed(ctrl_pressed) + event.get_modifiers_mask() + _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: + _push_warning_deprecated_arguments(shift_pressed, ctrl_pressed) + __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.set_alt_pressed(key_code == KEY_ALT) + event.set_shift_pressed(shift_pressed) + event.set_ctrl_pressed(ctrl_pressed) + _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: + var active_scene := scene() + if active_scene == null: + return self + 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 = active_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: + if scene() == null: + return "unknown" + 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 + if _last_input_event is InputEventKey and event is InputEventWithModifiers: + var last_input_event := _last_input_event as InputEventKey + var _event := event as InputEventWithModifiers + _event.shift_pressed = _event.shift_pressed or last_input_event.keycode == KEY_SHIFT + _event.alt_pressed = _event.alt_pressed or last_input_event.keycode == KEY_ALT + _event.ctrl_pressed = _event.ctrl_pressed or last_input_event.keycode == KEY_CTRL + _event.meta_pressed = _event.meta_pressed or last_input_event.keycode == KEY_META + + +# 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 + + +func _push_warning_deprecated_arguments(shift_pressed: bool, ctrl_pressed: bool) -> void: + if shift_pressed: + push_warning("Deprecated! Don't use 'shift_pressed' it will be removed in v7.0, checkout the documentaion how to use key combinations.") + if ctrl_pressed: + push_warning("Deprecated! Don't use 'ctrl_pressed' it will be removed in v7.0, checkout the documentaion how to use key combinations.") diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid new file mode 100644 index 0000000..74d54d1 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid @@ -0,0 +1 @@ +uid://bnfw1t8mifggc diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd new file mode 100644 index 0000000..549ad20 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd @@ -0,0 +1,460 @@ +@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" +const TEST_RERUN_UNTIL_FAILURE_RETRIES = GROUP_TEST + "/rerun_until_failure_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" + +# Godot GDScript warning settings +const CATEGORY_GDSCRIPT_WARNINGS := "debug/gdscript/warnings/" +const GDSCRIPT_WARNINGS_INFERRED_DECLARATION := CATEGORY_GDSCRIPT_WARNINGS + "inferred_declaration" +const GDSCRIPT_WARNINGS_EXCLUDE_ADDONS := CATEGORY_GDSCRIPT_WARNINGS + "exclude_addons" +const GDSCRIPT_WARNINGS_DIRECTORY_RULES := CATEGORY_GDSCRIPT_WARNINGS + "directory_rules" + +enum GdScriptWarningMode { + IGNORE = 0, + WARN = 1, + ERROR = 2, +} + +enum GdScriptWarningDirectoryMode { + EXCLUDE = 0, + INCLUDE = 1, +} + + +# 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 SHORTCUT_INSPECTOR_RERUN_TEST_UNTIL_FAILURE = GROUP_SHORTCUT_INSPECTOR + "/rerun_test_until_failure" + +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 :String = "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, +} + +static var _property_help :Dictionary[String, String] = {} + + +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") + create_property_if_need(TEST_RERUN_UNTIL_FAILURE_RETRIES, 10, "The number of reruns until the test fails.") + # 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]) + + +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_RERUN_TEST_UNTIL_FAILURE, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_UNTIL_FAILURE), "Rerun tests until failure occurs") + 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.RUN_TESTSUITE), "Run all test suites in the selected folder or file") + create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG), "Run all test suites in the selected folder or file (Debug)") + + +static func create_property_if_need( + property_name: String, + default_value: Variant, + help_text := "", + value_set := PackedStringArray()) -> void: + + if not ProjectSettings.has_setting(property_name): + #prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)]) + ProjectSettings.set_setting(property_name, default_value) + + ProjectSettings.set_initial_value(property_name, default_value) + set_property_info(property_name, default_value, value_set) + set_property_help(property_name, help_text) + + +static func set_property_info(property_name: String, value: Variant, value_set: PackedStringArray) -> void: + var info := { + "name": property_name, + "type": typeof(value), + "hint": PROPERTY_HINT_NONE, + "hint_string": "", + } + if not value_set.is_empty(): + info["hint"] = PROPERTY_HINT_ENUM + info["hint_string"] = ",".join(value_set) + ProjectSettings.add_property_info(info) + + +static func set_property_help(property_name: String, help_text: String) -> void: + _property_help[property_name] = help_text + + +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 get_rerun_max_retries() -> int: + return get_setting(TEST_RERUN_UNTIL_FAILURE_RETRIES, 10) + + +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 validate_is_inferred_declaration_enabled() -> GdUnitResult: + if ProjectSettings.get_setting(GDSCRIPT_WARNINGS_INFERRED_DECLARATION) == GdScriptWarningMode.IGNORE: + return GdUnitResult.success() + + if Engine.get_version_info().hex >= 0x40600: + var directory_rules: Dictionary = ProjectSettings.get_setting(GDSCRIPT_WARNINGS_DIRECTORY_RULES) + # Find the most specific matching rule (longest path wins) + var best_match := "" + for path: String in directory_rules.keys(): + if "res://addons/gdUnit4".begins_with(path) and path.length() > best_match.length(): + best_match = path + var is_excluded :bool = not best_match.is_empty() and directory_rules[best_match] == GdScriptWarningDirectoryMode.EXCLUDE + if not is_excluded: + return GdUnitResult.error(""" + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude the addon (debug/gdscript/warnings/directory_rules) + """.dedent().strip_edges()) + else: + if not ProjectSettings.get_setting(GDSCRIPT_WARNINGS_EXCLUDE_ADDONS): + return GdUnitResult.error(""" + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude addons (debug/gdscript/warnings/exclude_addons) + """.dedent().strip_edges()) + return GdUnitResult.success() + + +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 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 hint_string: String = property["hint_string"] + var value_set := PackedStringArray() if hint_string.is_empty() else hint_string.split(",") + var help_text :String = _property_help.get(property_name, "") + return GdUnitProperty.new(property_name, value_type, value, default, help_text, 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_property_help(new_property, help) + set_property_info(new_property, value, []) + ProjectSettings.clear(old_property) + prints("Successfully migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value]) diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd.uid b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid new file mode 100644 index 0000000..76bdc53 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid @@ -0,0 +1 @@ +uid://ci743q3rliql5 diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd new file mode 100644 index 0000000..528e133 --- /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 0000000..f2d54f3 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid @@ -0,0 +1 @@ +uid://foigcmhuiy2 diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd new file mode 100644 index 0000000..0cb6ad7 --- /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 : { +# signal_name : [signal_args], +# ... +# } +# } +var _collected_signals :Dictionary = {} + + +func clear() -> void: + for emitter :Object in _collected_signals.keys(): + if is_instance_valid(emitter): + unregister_emitter(emitter) + + +# connect to all possible signals defined by the emitter +# prepares the signal collection to store received signals and arguments +func register_emitter(emitter: Object, force_recreate := false) -> void: + if is_instance_valid(emitter): + # check emitter is already registerd + if _collected_signals.has(emitter): + if not force_recreate: + return + # If the flag recreate is set to true, emitters that are already registered must be deregistered before recreating, + # otherwise signals that have already been collected will be evaluated. + unregister_emitter(emitter) + + _collected_signals[emitter] = Dictionary() + # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. + if emitter is Node and !(emitter as Node).tree_exiting.is_connected(unregister_emitter): + (emitter as Node).tree_exiting.connect(unregister_emitter.bind(emitter)) + # connect to all signals of the emitter we want to collect + for signal_def in emitter.get_signal_list(): + var signal_name :String = signal_def["name"] + # set inital collected to empty + if not is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name] = Array() + if SIGNAL_BLACK_LIST.find(signal_name) != -1: + continue + if !emitter.is_connected(signal_name, _on_signal_emmited): + var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + if err != OK: + push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)]) + + +# unregister all acquired resources/connections, otherwise it ends up in orphans +# is called when the emitter is removed from the parent +func unregister_emitter(emitter :Object) -> void: + if is_instance_valid(emitter): + for signal_def in emitter.get_signal_list(): + var signal_name :String = signal_def["name"] + if emitter.is_connected(signal_name, _on_signal_emmited): + emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + @warning_ignore("return_value_discarded") + _collected_signals.erase(emitter) + + +# receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements +func _on_signal_emmited( + arg0 :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, + arg10 :Variant= NO_ARG, + arg11 :Variant= NO_ARG) -> void: + var signal_args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG) + # extract the emitter and signal_name from the last two arguments (see line 61 where is added) + var signal_name :String = signal_args.pop_back() + var emitter :Object = signal_args.pop_back() + #prints("_on_signal_emmited:", emitter, signal_name, signal_args) + if is_signal_collecting(emitter, signal_name): + @warning_ignore("unsafe_cast") + (_collected_signals[emitter][signal_name] as Array).append(signal_args) + + +func reset_received_signals(emitter: Object, signal_name: String, signal_args: Array) -> void: + #_debug_signal_list("before claer"); + if _collected_signals.has(emitter): + var signals_by_emitter :Dictionary = _collected_signals[emitter] + if signals_by_emitter.has(signal_name): + var received_args: Array = _collected_signals[emitter][signal_name] + # We iterate backwarts over to received_args to remove matching args. + # This will avoid array corruption see comment on `erase` otherwise we need a timeconsuming duplicate before + for arg_pos: int in range(received_args.size()-1, -1, -1): + var arg: Variant = received_args[arg_pos] + if GdObjects.equals(arg, signal_args): + received_args.remove_at(arg_pos) + #_debug_signal_list("after claer"); + + +func is_signal_collecting(emitter: Object, signal_name: String) -> bool: + @warning_ignore("unsafe_cast") + return _collected_signals.has(emitter) and (_collected_signals[emitter] as Dictionary).has(signal_name) + + +func match(emitter: Object, signal_name: String, args: Array) -> bool: + #prints("match", signal_name, _collected_signals[emitter][signal_name]); + if _collected_signals.is_empty() or not _collected_signals.has(emitter): + return false + for received_args :Variant in _collected_signals[emitter][signal_name]: + #prints("testing", signal_name, received_args, "vs", args) + if GdObjects.equals(received_args, args): + return true + return false + + +func _debug_signal_list(message :String) -> void: + prints("-----", message, "-------") + prints("senders {") + for emitter :Object in _collected_signals: + prints("\t", emitter) + for signal_name :String in _collected_signals[emitter]: + var args :Variant = _collected_signals[emitter][signal_name] + prints("\t\t", signal_name, args) + prints("}") diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid new file mode 100644 index 0000000..8ef15b2 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid @@ -0,0 +1 @@ +uid://coka6hobdska5 diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd b/addons/gdUnit4/src/core/GdUnitSignals.gd new file mode 100644 index 0000000..f0a900d --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd @@ -0,0 +1,118 @@ +class_name GdUnitSignals +extends RefCounted +## Singleton class that handles GdUnit's signal communication.[br] +## [br] +## This class manages all signals used to communicate test events, discovery, and status changes.[br] +## It uses a singleton pattern stored in Engine metadata to ensure a single instance.[br] +## [br] +## Signals are grouped by purpose:[br] +## - Client connection handling[br] +## - Test execution events[br] +## - Test discovery events[br] +## - Settings and status updates[br] +## [br] +## Example usage:[br] +## [codeblock] +## # Connect to test discovery +## GdUnitSignals.instance().gdunit_test_discovered.connect(self._on_test_discovered) +## +## # Emit test event +## GdUnitSignals.instance().gdunit_event.emit(test_event) +## [/codeblock] + + +## Emitted when a client connects to the GdUnit server.[br] +## [param client_id] The ID of the connected client. +@warning_ignore("unused_signal") +signal gdunit_client_connected(client_id: int) + + +## Emitted when a client disconnects from the GdUnit server.[br] +## [param client_id] The ID of the disconnected client. +@warning_ignore("unused_signal") +signal gdunit_client_disconnected(client_id: int) + + +## Emitted when a the user stops (terminates) the current test session +@warning_ignore("unused_signal") +signal gdunit_test_session_terminate() + + +## Emitted when a test execution event occurs.[br] +## [param event] The test event containing details about test execution. +@warning_ignore("unused_signal") +signal gdunit_event(event: GdUnitEvent) + + +## Emitted for test debug events during execution.[br] +## [param event] The debug event containing test execution details. +@warning_ignore("unused_signal") +signal gdunit_event_debug(event: GdUnitEvent) + + +## Emitted to broadcast a general message.[br] +## [param message] The message to broadcast. +@warning_ignore("unused_signal") +signal gdunit_message(message: String) + + +## Emitted to update test failure status.[br] +## [param is_failed] Whether the test has failed. +@warning_ignore("unused_signal") +signal gdunit_set_test_failed(is_failed: bool) + + +## Emitted when a GdUnit setting changes.[br] +## [param property] The property that was changed. +@warning_ignore("unused_signal") +signal gdunit_settings_changed(property: GdUnitProperty) + +## Called when a new test case is discovered during the discovery process. +## Custom implementations should connect to this signal and store the discovered test case as needed.[br] +## [param test_case] The discovered test case instance to be processed. +@warning_ignore("unused_signal") +signal gdunit_test_discover_added(test_case: GdUnitTestCase) + + +## Emitted when a test case is deleted.[br] +## [param test_case] The test case that was deleted. +@warning_ignore("unused_signal") +signal gdunit_test_discover_deleted(test_case: GdUnitTestCase) + + +## Emitted when a test case is modified.[br] +## [param test_case] The test case that was modified. +@warning_ignore("unused_signal") +signal gdunit_test_discover_modified(test_case: GdUnitTestCase) + + +const META_KEY := "GdUnitSignals" + + +## Returns the singleton instance of GdUnitSignals.[br] +## Creates a new instance if none exists.[br] +## [br] +## Returns: The GdUnitSignals singleton instance. +static func instance() -> GdUnitSignals: + if Engine.has_meta(META_KEY): + return Engine.get_meta(META_KEY) + var instance_ := GdUnitSignals.new() + Engine.set_meta(META_KEY, instance_) + return instance_ + + +## Cleans up the singleton instance and disconnects all signals.[br] +## [br] +## Should be called when GdUnit is shutting down or needs to reset.[br] +## Ensures proper cleanup of signal connections and resources. +static func dispose() -> void: + var signals := instance() + # cleanup connected signals + for signal_ in signals.get_signal_list(): + @warning_ignore("unsafe_cast") + for connection in signals.get_signal_connection_list(signal_["name"] as StringName): + var _signal: Signal = connection["signal"] + var _callable: Callable = connection["callable"] + _signal.disconnect(_callable) + signals = null + Engine.remove_meta(META_KEY) diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd.uid b/addons/gdUnit4/src/core/GdUnitSignals.gd.uid new file mode 100644 index 0000000..e0a7744 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd.uid @@ -0,0 +1 @@ +uid://bcsi62irwdk4p diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd b/addons/gdUnit4/src/core/GdUnitSingleton.gd new file mode 100644 index 0000000..b8e08cc --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -0,0 +1,56 @@ +################################################################################ +# Provides access to a global accessible singleton +# +# This is a workarount to the existing auto load singleton because of some bugs +# around plugin handling +################################################################################ +class_name GdUnitSingleton +extends Object + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const MEATA_KEY := "GdUnitSingletons" + + +static func instance(name: String, clazz: Callable) -> Variant: + if Engine.has_meta(name): + return Engine.get_meta(name) + var singleton: Variant = clazz.call() + if is_instance_of(singleton, RefCounted): + @warning_ignore("unsafe_cast") + push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, (singleton as RefCounted).get_class()]) + return + + Engine.set_meta(name, singleton) + GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton]) + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + @warning_ignore("return_value_discarded") + singletons.append(name) + Engine.set_meta(MEATA_KEY, singletons) + return singleton + + +static func unregister(p_singleton: String, use_call_deferred: bool = false) -> void: + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + if singletons.has(p_singleton): + GdUnitTools.prints_verbose("\n Unregister singleton '%s'" % p_singleton); + var index := singletons.find(p_singleton) + singletons.remove_at(index) + var instance_: Object = Engine.get_meta(p_singleton) + GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_]) + @warning_ignore("return_value_discarded") + GdUnitTools.free_instance(instance_, use_call_deferred) + Engine.remove_meta(p_singleton) + GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton) + Engine.set_meta(MEATA_KEY, singletons) + + +static func dispose(use_call_deferred: bool = false) -> void: + # use a copy because unregister is modify the singletons array + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + GdUnitTools.prints_verbose("----------------------------------------------------------------") + GdUnitTools.prints_verbose("Cleanup singletons %s" % singletons) + for singleton in PackedStringArray(singletons): + unregister(singleton, use_call_deferred) + Engine.remove_meta(MEATA_KEY) + GdUnitTools.prints_verbose("----------------------------------------------------------------") diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid b/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid new file mode 100644 index 0000000..ad5d47a --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid @@ -0,0 +1 @@ +uid://xyhj0vyjixln diff --git a/addons/gdUnit4/src/core/GdUnitStackTrace.gd b/addons/gdUnit4/src/core/GdUnitStackTrace.gd new file mode 100644 index 0000000..e1c2fd0 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitStackTrace.gd @@ -0,0 +1,114 @@ +## Captures and holds the test-side call stack at the point of an assertion failure.[br] +## Internal gdUnit4 frames are filtered out, leaving only user and test script frames. +class_name GdUnitStackTrace +extends RefCounted + +var _stack_trace: Array[GdUnitStackTraceElement] + + +static func from_script_backtraces(script_backtraces: Array[ScriptBacktrace]) -> GdUnitStackTrace: + var test_stack_trace: Array[GdUnitStackTraceElement] = [] + + for sb in script_backtraces: + for frame in sb.get_frame_count(): + var source := sb.get_frame_file(frame) + + if filter_sources(source): + continue + + var function := sb.get_frame_function(frame) + var line := sb.get_frame_line(frame) + test_stack_trace.append(GdUnitStackTraceElement.new(source, line, function)) + return GdUnitStackTrace.of(test_stack_trace) + + +func _init(stack_trace := _extract_test_stack_trace()) -> void: + _stack_trace = stack_trace + # Verify stacktrace + for stack_element in stack_trace: + assert(stack_element != null) + + +## Returns a newline-separated string of all frames in the stack trace. +func _to_string() -> String: + return "\n".join(_stack_trace) + + +## Returns all frames in the stack trace, ordered innermost first. +func get_frames() -> Array[GdUnitStackTraceElement]: + return _stack_trace + + +## Returns the line number of the topmost frame, or [code]-1[/code] if the stack trace is empty. +func get_line_number() -> int: + if _stack_trace.is_empty(): + return -1 + return _stack_trace.front()._line + + +## Returns a formatted multi-line string with each frame listed as [code]at - source:line in function 'name'[/code]. +func print_stack_trace() -> String: + var output := "" + for frame in _stack_trace: + if frame._function != null and not frame._function.is_empty(): + output += "\tat '%s' in %s:%d\n" % [frame._function, frame._source, frame._line] + else: + output += "\tat %s:%d\n" % [frame._source, frame._line] + return output + + +## Serializes the stack trace to a JSON string. +func serialize() -> String: + var frames: Array[Dictionary] = [] + for frame in _stack_trace: + frames.append({"source": frame._source, "line": frame._line, "function": frame._function}) + return JSON.stringify(frames) + + +## Reconstructs a [GdUnitStackTrace] from a JSON string produced by [method serialize]. +static func deserialize(json: String) -> GdUnitStackTrace: + var data: Variant = JSON.parse_string(json) + if not data is Array: + return GdUnitStackTrace.of([]) + var frames: Array[GdUnitStackTraceElement] = [] + for frame_data: Dictionary in data: + frames.append(GdUnitStackTraceElement.of(frame_data)) + return GdUnitStackTrace.of(frames) + + +## Creates a [GdUnitStackTrace] from an existing array of [GdUnitStackTraceElement] frames.[br] +## Useful for constructing expected stack traces in assertions. +static func of(stack_trace: Array[GdUnitStackTraceElement]) -> GdUnitStackTrace: + return GdUnitStackTrace.new(stack_trace) + + +## Captures the current GDScript call stack and filters out all internal gdUnit4 frames,[br] +## returning only frames from user scripts and test files. +static func _extract_test_stack_trace() -> Array[GdUnitStackTraceElement]: + var stack_trace: Array[Dictionary] = get_stack() + if stack_trace == null or stack_trace.is_empty(): + return [] + + var test_stack_trace: Array[GdUnitStackTraceElement] = [] + for index in range(stack_trace.size() - 1, -1, -1): + var stack_info := stack_trace[index] + var source: String = stack_info.get("source") + + if filter_sources(source): + continue + + var line: int = stack_info.get("line") + var function: String = stack_info.get("function") + + test_stack_trace.append(GdUnitStackTraceElement.new(source, line, function)) + + test_stack_trace.reverse() + return test_stack_trace + + + +static func filter_sources(source: String) -> bool: + return ( + source.begins_with("res://addons/gdUnit4/src/") + or source.begins_with("user://tmp/mock/") + or source.begins_with("user://tmp/spy/")) diff --git a/addons/gdUnit4/src/core/GdUnitStackTrace.gd.uid b/addons/gdUnit4/src/core/GdUnitStackTrace.gd.uid new file mode 100644 index 0000000..a552944 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitStackTrace.gd.uid @@ -0,0 +1 @@ +uid://cjyr672ithwi3 diff --git a/addons/gdUnit4/src/core/GdUnitStackTraceElement.gd b/addons/gdUnit4/src/core/GdUnitStackTraceElement.gd new file mode 100644 index 0000000..65df9cf --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitStackTraceElement.gd @@ -0,0 +1,32 @@ +## Represents a single frame in a GDScript call stack.[br] +## Stores the source file, line number, and function name at the point of capture. +class_name GdUnitStackTraceElement +extends RefCounted + + +## The resource path of the source file (e.g. [code]res://addons/my_plugin/foo.gd[/code]). +var _source: String +## The line number within [member _source]. +var _line: int +## The name of the function at this frame. +var _function: String + + +## Creates a new stack trace element from the given [param source] path, [param line] number, and [param function] name. +func _init(source: String, line: int, function: String) -> void: + _source = source + _line = line + _function = function + + +## Creates a [GdUnitStackTraceElement] from a dictionary with keys [code]source[/code], [code]line[/code], and [code]function[/code]. +static func of(data: Dictionary) -> GdUnitStackTraceElement: + var source: String = data["source"] + var line: int = data["line"] + var function: String = data["function"] + return GdUnitStackTraceElement.new(source, line, function) + + +## Returns a human-readable representation in the form [code]basename.function(file:line)[/code]. +func _to_string() -> String: + return "%s.%s(%s:%d)" % [_source.get_basename(), _function, _source.get_file(), _line] diff --git a/addons/gdUnit4/src/core/GdUnitStackTraceElement.gd.uid b/addons/gdUnit4/src/core/GdUnitStackTraceElement.gd.uid new file mode 100644 index 0000000..0c5c9c6 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitStackTraceElement.gd.uid @@ -0,0 +1 @@ +uid://bgx5l4casaxye diff --git a/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd new file mode 100644 index 0000000..33b80e2 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd @@ -0,0 +1,97 @@ +class_name GdUnitTestResourceLoader +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +enum { + GD_SUITE, + CS_SUITE +} + + +static func load_test_suite(resource_path: String, script_type := GD_SUITE) -> Node: + match script_type: + GD_SUITE: + return load_test_suite_gd(resource_path) + CS_SUITE: + return load_test_suite_cs(resource_path) + assert("type '%s' is not implemented" % script_type) + return null + + +static func load_tests(resource_path: String) -> Dictionary: + var script := load_gd_script(resource_path) + var discovered_tests := {} + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + discovered_tests[test.display_name] = test + ) + + return discovered_tests + + +static func load_test_suite_gd(resource_path: String) -> GdUnitTestSuite: + var script := load_gd_script(resource_path) + var discovered_tests: Array[GdUnitTestCase] = [] + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + discovered_tests.append(test) + ) + # complete test suite wiht parsed test cases + return GdUnitTestSuiteScanner.new().load_suite(script, discovered_tests) + + +static func load_test_suite_cs(resource_path: String) -> Node: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return null + var script :Script = ClassDB.instantiate("CSharpScript") + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + script.resource_path = resource_path + script.reload() + return null + + +static func load_cs_script(resource_path: String, debug_write := false) -> Script: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return null + var script :Script = ClassDB.instantiate("CSharpScript") + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + var script_resource_path := resource_path.replace(resource_path.get_extension(), "cs") + if debug_write: + script_resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % script_resource_path.get_file() + print_debug("save resource:", script_resource_path) + DirAccess.remove_absolute(script_resource_path) + var err := ResourceSaver.save(script, script_resource_path) + if err != OK: + print_debug("Can't save debug resource",script_resource_path, "Error:", error_string(err)) + script.take_over_path(script_resource_path) + else: + script.take_over_path(resource_path) + script.reload() + return script + + +static func load_gd_script(resource_path: String, debug_write := false) -> GDScript: + # grap current level + var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access") + # disable and load the script + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0) + + var script := GDScript.new() + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + var script_resource_path := resource_path.replace(resource_path.get_extension(), "gd") + if debug_write: + script_resource_path = script_resource_path.replace("res://", GdUnitFileAccess.temp_dir() + "/") + #print_debug("save resource: ", script_resource_path) + DirAccess.remove_absolute(script_resource_path) + DirAccess.make_dir_recursive_absolute(script_resource_path.get_base_dir()) + var err := ResourceSaver.save(script, script_resource_path, ResourceSaver.FLAG_REPLACE_SUBRESOURCE_PATHS) + if err != OK: + print_debug("Can't save debug resource", script_resource_path, "Error:", error_string(err)) + script.take_over_path(script_resource_path) + else: + script.take_over_path(resource_path) + var error := script.reload() + if error != OK: + push_error("Errors on loading script %s. Error: %s" % [resource_path, error_string(error)]) + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access) + return script + #@warning_ignore("unsafe_cast") diff --git a/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid new file mode 100644 index 0000000..8ecf4c0 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid @@ -0,0 +1 @@ +uid://d2b10ip4vtmvq diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd new file mode 100644 index 0000000..d3637e0 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -0,0 +1,20 @@ +class_name GdUnitTestSuiteBuilder +extends RefCounted + + +static func create(source :Script, line_number :int) -> GdUnitResult: + var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder()) + # we need to save and close the testsuite and source if is current opened before modify + @warning_ignore("return_value_discarded") + GdUnitScriptEditorControls.save_an_open_script(source.resource_path) + @warning_ignore("return_value_discarded") + GdUnitScriptEditorControls.save_an_open_script(test_suite_path, true) + if source.get_class() == "CSharpScript": + return GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path) + var parser := GdScriptParser.new() + var lines := source.source_code.split("\n") + var current_line := lines[line_number] + var func_name := parser.parse_func_name(current_line) + if func_name.is_empty(): + return GdUnitResult.error("No function found at line: %d." % line_number) + return GdUnitTestSuiteScanner.create_test_case(test_suite_path, func_name, source.resource_path) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid new file mode 100644 index 0000000..552bf4e --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid @@ -0,0 +1 @@ +uid://moik8syykuo8 diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd new file mode 100644 index 0000000..f371cb1 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd @@ -0,0 +1,410 @@ +class_name GdUnitTestSuiteScanner +extends RefCounted + +const TEST_FUNC_TEMPLATE =""" + +func test_${func_name}() -> void: + # remove this line and complete your test + assert_not_yet_implemented() +""" + + +# we exclude the gdunit source directorys by default +const exclude_scan_directories = [ + "res://addons/gdUnit4/bin", + "res://addons/gdUnit4/src", + "res://reports"] + + +const ARGUMENT_TIMEOUT := "timeout" +const ARGUMENT_SKIP := "do_skip" +const ARGUMENT_SKIP_REASON := "skip_reason" +const ARGUMENT_PARAMETER_SET := "test_parameters" + + +var _script_parser := GdScriptParser.new() +var _included_resources: PackedStringArray = [] +var _excluded_resources: PackedStringArray = [] +var _expression_runner := GdUnitExpressionRunner.new() +var _regex_extends_clazz_name := RegEx.create_from_string("extends[\\s]+([\\S]+)") + + +func prescan_testsuite_classes() -> void: + # scan and cache extends GdUnitTestSuite by class name an resource paths + var script_classes: Array[Dictionary] = ProjectSettings.get_global_class_list() + for script_meta in script_classes: + var base_class: String = script_meta["base"] + var resource_path: String = script_meta["path"] + if base_class == "GdUnitTestSuite": + @warning_ignore("return_value_discarded") + _included_resources.append(resource_path) + elif ClassDB.class_exists(base_class): + @warning_ignore("return_value_discarded") + _excluded_resources.append(resource_path) + + +func scan(resource_path: String) -> Array[Script]: + prescan_testsuite_classes() + # if single testsuite requested + if FileAccess.file_exists(resource_path): + var test_suite := _load_is_test_suite(resource_path) + if test_suite != null: + return [test_suite] + return [] + return scan_directory(resource_path) + + +func scan_directory(resource_path: String) -> Array[Script]: + prescan_testsuite_classes() + # We use the global cache to fast scan for test suites. + if _excluded_resources.has(resource_path): + return [] + + var base_dir := DirAccess.open(resource_path) + if base_dir == null: + prints("Given directory or file does not exists:", resource_path) + return [] + + prints("Scanning for test suites in:", resource_path) + return _scan_test_suites_scripts(base_dir, []) + + +func _scan_test_suites_scripts(dir: DirAccess, collected_suites: Array[Script]) -> Array[Script]: + # Skip excluded directories + if dir.file_exists(".gdignore"): + prints("Exclude directory %s, containing .gdignore file" % dir.get_current_dir()) + return [] + + if exclude_scan_directories.has(dir.get_current_dir()): + return collected_suites + + var err := dir.list_dir_begin() + if err != OK: + push_error("Error on scanning directory %s" % dir.get_current_dir(), error_string(err)) + return collected_suites + var file_name := dir.get_next() + while file_name != "": + var resource_path := GdUnitTestSuiteScanner._file(dir, file_name) + if dir.current_is_dir(): + var sub_dir := DirAccess.open(resource_path) + if sub_dir != null: + @warning_ignore("return_value_discarded") + _scan_test_suites_scripts(sub_dir, collected_suites) + else: + var time := LocalTime.now() + var test_suite := _load_is_test_suite(resource_path) + if test_suite: + collected_suites.append(test_suite) + if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300: + push_warning("Scanning of test-suite '%s' took more than 300ms: " % resource_path, time.elapsed_since()) + file_name = dir.get_next() + return collected_suites + + +static func _file(dir: DirAccess, file_name: String) -> String: + var current_dir := dir.get_current_dir() + if current_dir.ends_with("/"): + return current_dir + file_name + return current_dir + "/" + file_name + + +func _load_is_test_suite(resource_path: String) -> Script: + if not GdUnitTestSuiteScanner._is_script_format_supported(resource_path): + return null + + # We use the global cache to fast scan for test suites. + if _excluded_resources.has(resource_path): + return null + # Check in the global class cache whether the GdUnitTestSuite class has been extended. + if _included_resources.has(resource_path): + return GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path) + + # Otherwise we need to scan manual, we need to exclude classes where direct extends form Godot classes + # the resource loader can fail to load e.g. plugin classes with do preload other scripts + #var extends_from := get_extends_classname(resource_path) + # If not extends is defined or extends from a Godot class + #if extends_from.is_empty() or ClassDB.class_exists(extends_from): + # return null + # Finally, we need to load the class to determine it is a test suite + var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path) + if not is_test_suite(script): + return null + return script + + +func load_suite(script: GDScript, tests: Array[GdUnitTestCase]) -> GdUnitTestSuite: + var test_suite: GdUnitTestSuite = script.new() + var first_test: GdUnitTestCase = tests.front() + test_suite.set_name(first_test.suite_name) + + # We need to group first all parameterized tests together to load the parameter set once + var grouped_by_test := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.test_name + ) + # Extract function descriptors + var test_names: PackedStringArray = grouped_by_test.keys() + test_names.append("before") + var function_descriptors := _script_parser.get_function_descriptors(script, test_names) + + # Convert to test + for fd in function_descriptors: + if fd.name() == "before": + _handle_test_suite_arguments(test_suite, script, fd) + continue + + # Build test attributes from test method + var test_attribute := _build_test_attribute(script, fd) + # Create test from descriptor and given attributes + var test_group: Array = grouped_by_test[fd.name()] + for test: GdUnitTestCase in test_group: + # We need a copy, because of mutable state + var attribute: TestCaseAttribute = test_attribute.clone() + test_suite.add_child(_TestCase.new(test, attribute, fd)) + return test_suite + + +func _build_test_attribute(script: GDScript, fd: GdFunctionDescriptor) -> TestCaseAttribute: + var collected_unknown_aruments := PackedStringArray() + var attribute := TestCaseAttribute.new() + + # Collect test attributes + for arg: GdFunctionArgument in fd.args(): + if arg.type() == GdObjects.TYPE_FUZZER: + attribute.fuzzers.append(arg) + else: + # We allow underscore as prefix to prevent unused argument warnings + match arg.name().trim_prefix("_"): + ARGUMENT_TIMEOUT: + attribute.timeout = type_convert(arg.default(), TYPE_INT) + ARGUMENT_SKIP: + var result: Variant = _expression_runner.execute(script, arg.plain_value()) + if result is bool: + attribute.is_skipped = result + else: + push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value()) + ARGUMENT_SKIP_REASON: + attribute.skip_reason = arg.plain_value() + Fuzzer.ARGUMENT_ITERATIONS: + attribute.fuzzer_iterations = type_convert(arg.default(), TYPE_INT) + Fuzzer.ARGUMENT_SEED: + attribute.test_seed = type_convert(arg.default(), TYPE_INT) + ARGUMENT_PARAMETER_SET: + collected_unknown_aruments.clear() + pass + _: + collected_unknown_aruments.append(arg.name()) + + # Verify for unknown arguments + if not collected_unknown_aruments.is_empty(): + attribute.is_skipped = true + attribute.skip_reason = "Unknown test case argument's %s found." % collected_unknown_aruments + + return attribute + + +# We load the test suites with disabled unsafe_method_access to avoid spamming loading errors +# `unsafe_method_access` will happen when using `assert_that` +static func load_with_disabled_warnings(resource_path: String) -> Script: + # grap current level + var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access") + + # disable and load the script + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0) + + var script: Script = ( + GdUnitTestResourceLoader.load_gd_script(resource_path) if resource_path.ends_with("resource") + else ResourceLoader.load(resource_path)) + + # restore + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access) + return script + + +static func is_test_suite(script: Script) -> bool: + if script is GDScript: + var stack := [script] + while not stack.is_empty(): + var current: Script = stack.pop_front() + var base: Script = current.get_base_script() + if base != null: + if base.resource_path.find("GdUnitTestSuite") != -1: + return true + stack.push_back(base) + elif script != null and script.get_class() == "CSharpScript": + return true + return false + + +static func _is_script_format_supported(resource_path: String) -> bool: + var ext := resource_path.get_extension() + return ext == "gd" or (ext == "cs" and ClassDB.class_exists("CSharpScript")) + + +static func parse_test_suite_name(script: Script) -> String: + return script.resource_path.get_file().replace(".gd", "") + + +func _handle_test_suite_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void: + for arg in fd.args(): + # We allow underscore as prefix to prevent unused argument warnings + match arg.name().trim_prefix("_"): + ARGUMENT_SKIP: + var result: Variant = _expression_runner.execute(script, arg.plain_value()) + if result is bool: + test_suite.__is_skipped = result + else: + push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value()) + ARGUMENT_SKIP_REASON: + test_suite.__skip_reason = arg.plain_value() + _: + push_error("Unsuported argument `%s` found on before() at '%s'!" % [arg.name(), script.resource_path]) + + +# converts given file name by configured naming convention +static func _to_naming_convention(file_name: String) -> String: + var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, 0) + match nc: + GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT: + if GdObjects.is_snake_case(file_name): + return GdObjects.to_snake_case(file_name + "Test") + return GdObjects.to_pascal_case(file_name + "Test") + GdUnitSettings.NAMING_CONVENTIONS.SNAKE_CASE: + return GdObjects.to_snake_case(file_name + "Test") + GdUnitSettings.NAMING_CONVENTIONS.PASCAL_CASE: + return GdObjects.to_pascal_case(file_name + "Test") + push_error("Unexpected case") + return "--" + + +static func resolve_test_suite_path(source_script_path: String, test_root_folder: String = "test") -> String: + var file_name := source_script_path.get_basename().get_file() + var suite_name := _to_naming_convention(file_name) + if test_root_folder.is_empty() or test_root_folder == "/": + return source_script_path.replace(file_name, suite_name) + + # is user tmp + if source_script_path.begins_with("user://tmp"): + return normalize_path(source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder)).replace(file_name, suite_name) + + # at first look up is the script under a "src" folder located + var test_suite_path: String + var src_folder := source_script_path.find("/src/") + if src_folder != -1: + test_suite_path = source_script_path.replace("/src/", "/"+test_root_folder+"/") + else: + var paths := source_script_path.split("/", false) + # is a plugin script? + if paths[1] == "addons": + test_suite_path = "%s//addons/%s/%s" % [paths[0], paths[2], test_root_folder] + # rebuild plugin path + for index in range(3, paths.size()): + test_suite_path += "/" + paths[index] + else: + test_suite_path = paths[0] + "//" + test_root_folder + for index in range(1, paths.size()): + test_suite_path += "/" + paths[index] + return normalize_path(test_suite_path).replace(file_name, suite_name) + + +static func normalize_path(path: String) -> String: + return path.replace("///", "/") + + +static func create_test_suite(test_suite_path: String, source_path: String) -> GdUnitResult: + # create directory if not exists + if not DirAccess.dir_exists_absolute(test_suite_path.get_base_dir()): + var error_ := DirAccess.make_dir_recursive_absolute(test_suite_path.get_base_dir()) + if error_ != OK: + return GdUnitResult.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error_]) + var script := GDScript.new() + script.source_code = GdUnitTestSuiteTemplate.build_template(source_path) + var error := ResourceSaver.save(script, test_suite_path) + if error != OK: + return GdUnitResult.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error]) + return GdUnitResult.success(test_suite_path) + + +static func get_test_case_line_number(resource_path: String, func_name: String) -> int: + var file := FileAccess.open(resource_path, FileAccess.READ) + if file != null: + var line_number := 0 + while not file.eof_reached(): + var row := file.get_line() + line_number += 1 + # ignore comments and empty lines and not test functions + if row.begins_with("#") || row.length() == 0 || row.find("func test_") == -1: + continue + # abort if test case name found + if row.find("func") != -1 and row.find("test_" + func_name) != -1: + return line_number + return -1 + + +func get_extends_classname(resource_path: String) -> String: + var file := FileAccess.open(resource_path, FileAccess.READ) + if file != null: + while not file.eof_reached(): + var row := file.get_line() + # skip comments and empty lines + if row.begins_with("#") || row.length() == 0: + continue + # Stop at first function + if row.contains("func"): + return "" + var result := _regex_extends_clazz_name.search(row) + if result != null: + return result.get_string(1) + return "" + + +static func add_test_case(resource_path: String, func_name: String) -> GdUnitResult: + var script := load_with_disabled_warnings(resource_path) + # count all exiting lines and add two as space to add new test case + var line_number := count_lines(script) + 2 + var func_body := TEST_FUNC_TEMPLATE.replace("${func_name}", func_name) + if Engine.is_editor_hint(): + # NOTE: Avoid using EditorInterface and EditorSettings directly, + # as it causes compilation errors in exported projects. + @warning_ignore_start("unsafe_method_access") + var editor_interface: Object = Engine.get_singleton("EditorInterface") + var settings: Object = editor_interface.get_editor_settings() + var ident_type: int = settings.get_setting("text_editor/behavior/indent/type") + var ident_size: int = settings.get_setting("text_editor/behavior/indent/size") + @warning_ignore_restore("unsafe_method_access") + if ident_type == 1: + func_body = func_body.replace(" ", "".lpad(ident_size, " ")) + script.source_code += func_body + var error := ResourceSaver.save(script, resource_path) + if error != OK: + return GdUnitResult.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error]) + return GdUnitResult.success({ "path" : resource_path, "line" : line_number}) + + +static func count_lines(script: Script) -> int: + return script.source_code.split("\n").size() + + +static func test_suite_exists(test_suite_path: String) -> bool: + return FileAccess.file_exists(test_suite_path) + + +static func test_case_exists(test_suite_path :String, func_name :String) -> bool: + if not test_suite_exists(test_suite_path): + return false + var script := load_with_disabled_warnings(test_suite_path) + for f in script.get_script_method_list(): + if f["name"] == "test_" + func_name: + return true + return false + + +static func create_test_case(test_suite_path: String, func_name: String, source_script_path: String) -> GdUnitResult: + if test_case_exists(test_suite_path, func_name): + var line_number := get_test_case_line_number(test_suite_path, func_name) + return GdUnitResult.success({ "path" : test_suite_path, "line" : line_number}) + + if not test_suite_exists(test_suite_path): + var result := create_test_suite(test_suite_path, source_script_path) + if result.is_error(): + return result + return add_test_case(test_suite_path, func_name) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid new file mode 100644 index 0000000..e2700e8 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid @@ -0,0 +1 @@ +uid://dlqbf4tkrc4p8 diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd new file mode 100644 index 0000000..bf78da7 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -0,0 +1,145 @@ +extends RefCounted + + +static var _richtext_normalize: RegEx + + +static func normalize_text(text :String) -> String: + return text.replace("\r", ""); + + +static func richtext_normalize(input :String) -> String: + if _richtext_normalize == null: + _richtext_normalize = to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]") + return _richtext_normalize.sub(input, "", true).replace("\r", "") + + +static func to_regex(pattern :String) -> RegEx: + var regex := RegEx.new() + var err := regex.compile(pattern) + if err != OK: + push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, error_string(err)]) + return regex + + +static func prints_verbose(message :String) -> void: + if OS.is_stdout_verbose(): + prints(message) + + +static func free_instance(instance :Variant, use_call_deferred :bool = false, is_stdout_verbose := false) -> bool: + if instance is Array: + var as_array: Array = instance + for element: Variant in as_array: + @warning_ignore("return_value_discarded") + free_instance(element) + as_array.clear() + return true + # do not free an already freed instance + if not is_instance_valid(instance): + return false + # do not free a class refernece + @warning_ignore("unsafe_cast") + if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"): + return false + if is_stdout_verbose: + print_verbose("GdUnit4:gc():free instance ", instance) + @warning_ignore("unsafe_cast") + release_double(instance as Object) + if instance is RefCounted: + @warning_ignore("unsafe_cast") + (instance as RefCounted).notification(Object.NOTIFICATION_PREDELETE) + # If scene runner freed we explicit await all inputs are processed + if instance is GdUnitSceneRunnerImpl: + @warning_ignore("unsafe_cast") + await (instance as GdUnitSceneRunnerImpl).await_input_processed() + return true + else: + if instance is Timer: + var timer: Timer = instance + timer.stop() + if use_call_deferred: + timer.call_deferred("free") + else: + timer.free() + await (Engine.get_main_loop() as SceneTree).process_frame + return true + + @warning_ignore("unsafe_cast") + if instance is Node and (instance as Node).get_parent() != null: + var node: Node = instance + if is_stdout_verbose: + print_verbose("GdUnit4:gc():remove node from parent ", node.get_parent(), node) + if use_call_deferred: + node.get_parent().remove_child.call_deferred(node) + #instance.call_deferred("set_owner", null) + else: + node.get_parent().remove_child(node) + if is_stdout_verbose: + print_verbose("GdUnit4:gc():freeing `free()` the instance ", instance) + if use_call_deferred: + @warning_ignore("unsafe_cast") + (instance as Object).call_deferred("free") + else: + @warning_ignore("unsafe_cast") + (instance as Object).free() + return !is_instance_valid(instance) + + +static func _release_connections(instance :Object) -> void: + if is_instance_valid(instance): + # disconnect from all connected signals to force freeing, otherwise it ends up in orphans + for connection in instance.get_incoming_connections(): + var signal_ :Signal = connection["signal"] + var callable_ :Callable = connection["callable"] + #prints(instance, connection) + #prints("signal", signal_.get_name(), signal_.get_object()) + #prints("callable", callable_.get_object()) + if instance.has_signal(signal_.get_name()) and instance.is_connected(signal_.get_name(), callable_): + #prints("disconnect signal", signal_.get_name(), callable_) + instance.disconnect(signal_.get_name(), callable_) + release_timers() + + +static func release_timers() -> void: + # we go the new way to hold all gdunit timers in group 'GdUnitTimers' + var scene_tree := Engine.get_main_loop() as SceneTree + if scene_tree.root == null: + return + for node :Node in scene_tree.root.get_children(): + if is_instance_valid(node) and node.is_in_group("GdUnitTimers"): + if is_instance_valid(node): + scene_tree.root.remove_child.call_deferred(node) + (node as Timer).stop() + node.queue_free() + + +# the finally cleaup unfreed resources and singletons +static func dispose_all(use_call_deferred :bool = false) -> void: + prints("Run dispose test resources") + release_timers() + GdUnitSingleton.dispose(use_call_deferred) + GdUnitSignals.dispose() + + +# if instance an mock or spy we need manually freeing the self reference +static func release_double(instance :Object) -> void: + if instance.has_method("__release_double"): + instance.call("__release_double") + + + +static func find_test_case(test_suite: Node, test_case_name: String, index := -1) -> _TestCase: + for test_case: _TestCase in test_suite.get_children(): + if test_case.test_name() == test_case_name: + if index != -1: + if test_case._test_case.attribute_index != index: + continue + return test_case + return null + + +static func register_expect_interupted_by_timeout(test_suite: Node, test_case_name: String) -> void: + var test_case := find_test_case(test_suite, test_case_name) + if test_case: + test_case.expect_to_interupt() diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd.uid b/addons/gdUnit4/src/core/GdUnitTools.gd.uid new file mode 100644 index 0000000..55e94a5 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTools.gd.uid @@ -0,0 +1 @@ +uid://bnkrdpet70kgs diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd b/addons/gdUnit4/src/core/GodotVersionFixures.gd new file mode 100644 index 0000000..5299325 --- /dev/null +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd @@ -0,0 +1,11 @@ +## This service class contains helpers to wrap Godot functions and handle them carefully depending on the current Godot version +class_name GodotVersionFixures +extends RefCounted + + +# handle global_position fixed by https://github.com/godotengine/godot/pull/88473 +static func set_event_global_position(event: InputEventMouseMotion, global_position: Vector2) -> void: + if Engine.get_version_info().hex >= 0x40202 or Engine.get_version_info().hex == 0x40104: + event.global_position = event.position + else: + event.global_position = global_position diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid b/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid new file mode 100644 index 0000000..4a85f1b --- /dev/null +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid @@ -0,0 +1 @@ +uid://c5fbpp5l1v11n diff --git a/addons/gdUnit4/src/core/LocalTime.gd b/addons/gdUnit4/src/core/LocalTime.gd new file mode 100644 index 0000000..fabaaf6 --- /dev/null +++ b/addons/gdUnit4/src/core/LocalTime.gd @@ -0,0 +1,114 @@ +# This class provides Date/Time functionallity to Godot +class_name LocalTime +extends Resource + +enum TimeUnit { + DEFAULT = 0, + MILLIS = 1, + SECOND = 2, + MINUTE = 3, + HOUR = 4, + DAY = 5, + MONTH = 6, + YEAR = 7 +} + +const SECONDS_PER_MINUTE:int = 60 +const MINUTES_PER_HOUR:int = 60 +const HOURS_PER_DAY:int = 24 +const MILLIS_PER_SECOND:int = 1000 +const MILLIS_PER_MINUTE:int = MILLIS_PER_SECOND * SECONDS_PER_MINUTE +const MILLIS_PER_HOUR:int = MILLIS_PER_MINUTE * MINUTES_PER_HOUR + +var _time :int +var _hour :int +var _minute :int +var _second :int +var _millisecond :int + + +static func now() -> LocalTime: + return LocalTime.new(_get_system_time_msecs()) + + +static func of_unix_time(time_ms :int) -> LocalTime: + return LocalTime.new(time_ms) + + +static func local_time(hours :int, minutes :int, seconds :int, milliseconds :int) -> LocalTime: + return LocalTime.new(MILLIS_PER_HOUR * hours\ + + MILLIS_PER_MINUTE * minutes\ + + MILLIS_PER_SECOND * seconds\ + + milliseconds) + + +func elapsed_since() -> String: + return LocalTime.elapsed(LocalTime._get_system_time_msecs() - _time) + + +func elapsed_since_ms() -> int: + return LocalTime._get_system_time_msecs() - _time + + +func plus(time_unit :TimeUnit, value :int) -> LocalTime: + var addValue:int = 0 + match time_unit: + TimeUnit.MILLIS: + addValue = value + TimeUnit.SECOND: + addValue = value * MILLIS_PER_SECOND + TimeUnit.MINUTE: + addValue = value * MILLIS_PER_MINUTE + TimeUnit.HOUR: + addValue = value * MILLIS_PER_HOUR + @warning_ignore("return_value_discarded") + _init(_time + addValue) + return self + + +static func elapsed(p_time_ms :int) -> String: + var local_time_ := LocalTime.new(p_time_ms) + if local_time_._hour > 0: + return "%dh %dmin %ds %dms" % [local_time_._hour, local_time_._minute, local_time_._second, local_time_._millisecond] + if local_time_._minute > 0: + return "%dmin %ds %dms" % [local_time_._minute, local_time_._second, local_time_._millisecond] + if local_time_._second > 0: + return "%ds %dms" % [local_time_._second, local_time_._millisecond] + return "%dms" % local_time_._millisecond + + +# create from epoch timestamp in ms +func _init(time: int) -> void: + _time = time + @warning_ignore("integer_division") + _hour = (time / MILLIS_PER_HOUR) % 24 + @warning_ignore("integer_division") + _minute = (time / MILLIS_PER_MINUTE) % 60 + @warning_ignore("integer_division") + _second = (time / MILLIS_PER_SECOND) % 60 + _millisecond = time % 1000 + + +func hour() -> int: + return _hour + + +func minute() -> int: + return _minute + + +func second() -> int: + return _second + + +func millis() -> int: + return _millisecond + + +func _to_string() -> String: + return "%02d:%02d:%02d.%03d" % [_hour, _minute, _second, _millisecond] + + +# wraper to old OS.get_system_time_msecs() function +static func _get_system_time_msecs() -> int: + return Time.get_unix_time_from_system() * 1000 as int diff --git a/addons/gdUnit4/src/core/LocalTime.gd.uid b/addons/gdUnit4/src/core/LocalTime.gd.uid new file mode 100644 index 0000000..a176943 --- /dev/null +++ b/addons/gdUnit4/src/core/LocalTime.gd.uid @@ -0,0 +1 @@ +uid://u7ywj5ixlsio diff --git a/addons/gdUnit4/src/core/_TestCase.gd b/addons/gdUnit4/src/core/_TestCase.gd new file mode 100644 index 0000000..49f64a1 --- /dev/null +++ b/addons/gdUnit4/src/core/_TestCase.gd @@ -0,0 +1,258 @@ +class_name _TestCase +extends Node + +signal completed() + + +var _test_case: GdUnitTestCase +var _attribute: TestCaseAttribute +var _current_iteration: int = -1 +var _expect_to_interupt := false +var _timer: Timer +var _interupted := false +var _terminated := false +var _failed := false +var _parameter_set_resolver: GdUnitTestParameterSetResolver +var _is_disposed := false +var _func_state: Variant + + +func _init(test_case: GdUnitTestCase, attribute: TestCaseAttribute, fd: GdFunctionDescriptor) -> void: + _test_case = test_case + _attribute = attribute + set_function_descriptor(fd) + + +func execute(p_test_parameter := Array(), p_iteration := 0) -> void: + _failure_received(false) + _current_iteration = p_iteration - 1 + if _current_iteration == - 1: + _set_failure_handler() + set_timeout() + + if is_parameterized(): + execute_parameterized() + elif not p_test_parameter.is_empty(): + update_fuzzers(p_test_parameter, p_iteration) + _execute_test_case(test_name(), p_test_parameter) + else: + _execute_test_case(test_name(), []) + await completed + + +func execute_parameterized() -> void: + _failure_received(false) + set_timeout() + + # Resolve parameter set at runtime to include runtime variables + var test_parameters := await _resolve_test_parameters(_test_case.attribute_index) + if test_parameters.is_empty(): + return + + await _execute_test_case(test_name(), test_parameters) + + +func _resolve_test_parameters(attribute_index: int) -> Array: + var result := _parameter_set_resolver.load_parameter_sets(get_parent()) + if result.is_error(): + do_skip(true, result.error_message()) + await (Engine.get_main_loop() as SceneTree).process_frame + completed.emit() + return [] + + # validate the parameter set + var parameter_sets: Array = result.value() + result = _parameter_set_resolver.validate(parameter_sets, attribute_index) + if result.is_error(): + do_skip(true, result.error_message()) + await (Engine.get_main_loop() as SceneTree).process_frame + completed.emit() + return [] + + @warning_ignore("unsafe_method_access") + var test_parameters: Array = parameter_sets[attribute_index].duplicate() + # We need here to add a empty array to override the `test_parameters` to prevent initial "default" parameters from being used. + # This prevents objects in the argument list from being unnecessarily re-instantiated. + test_parameters.append([]) + + return test_parameters + + +func dispose() -> void: + if _is_disposed: + return + _is_disposed = true + Engine.remove_meta("GD_TEST_FAILURE") + stop_timer() + _remove_failure_handler() + _attribute.fuzzers.clear() + + +@warning_ignore("shadowed_variable_base_class", "redundant_await") +func _execute_test_case(name: String, test_parameter: Array) -> void: + # save the function state like GDScriptFunctionState to dispose at test timeout to prevent orphan state + _func_state = get_parent().callv(name, test_parameter) + await _func_state + # Give the engine time to free resources otherwies we do orphan false detection + await get_tree().process_frame + # We need to call deferred the signal `completed` otherwise the current thread is blocked + test_completed.call_deferred() + + +func test_completed() -> void: + completed.emit() + + +func update_fuzzers(input_values: Array, iteration: int) -> void: + for fuzzer :Variant in input_values: + if fuzzer is Fuzzer: + fuzzer._iteration_index = iteration + 1 + + +func set_timeout() -> void: + if is_instance_valid(_timer): + return + var time: float = _attribute.timeout / 1000.0 + _timer = Timer.new() + add_child(_timer) + _timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id()) + @warning_ignore("return_value_discarded") + _timer.timeout.connect(do_interrupt, CONNECT_DEFERRED) + _timer.set_one_shot(true) + _timer.set_wait_time(time) + _timer.set_autostart(false) + _timer.start() + + +func do_interrupt() -> void: + _interupted = true + # We need to dispose manually the function state here + GdObjects.dispose_function_state(_func_state) + if not is_expect_interupted(): + var execution_context := GdUnitThreadManager.get_current_context().get_execution_context() + if is_fuzzed(): + execution_context.add_report(GdUnitReport.new()\ + .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout"))) + else: + execution_context.add_report(GdUnitReport.new()\ + .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(_attribute.timeout))) + test_completed.call_deferred() + + +func do_terminate() -> void: + _terminated = true + # We need to dispose manually the function state here + GdObjects.dispose_function_state(_func_state) + var execution_context := GdUnitThreadManager.get_current_context().get_execution_context() + execution_context.add_report(GdUnitReport.new()\ + .create(GdUnitReport.TERMINATED, line_number(), GdAssertMessages.test_session_terminated())) + test_completed.call_deferred() + + +func _set_failure_handler() -> void: + if not GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received): + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_set_test_failed.connect(_failure_received) + + +func _remove_failure_handler() -> void: + if GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received): + GdUnitSignals.instance().gdunit_set_test_failed.disconnect(_failure_received) + + +func _failure_received(is_failed: bool) -> void: + # is already failed? + if _failed: + return + _failed = is_failed + Engine.set_meta("GD_TEST_FAILURE", is_failed) + + +func stop_timer() -> void: + # finish outstanding timeouts + if is_instance_valid(_timer): + _timer.stop() + _timer.call_deferred("free") + _timer = null + + +func expect_to_interupt() -> void: + _expect_to_interupt = true + + +func is_interupted() -> bool: + return _interupted + + +func is_expect_interupted() -> bool: + return _expect_to_interupt + + +func is_terminated() -> bool: + return _terminated + + +func is_parameterized() -> bool: + return _parameter_set_resolver.is_parameterized() + + +func is_skipped() -> bool: + return _attribute.is_skipped + + +func skip_info() -> String: + return _attribute.skip_reason + + +func id() -> GdUnitGUID: + return _test_case.guid + + +func test_name() -> String: + return _test_case.test_name + + +func line_number() -> int: + return _test_case.line_number + + +func iterations() -> int: + return _attribute.fuzzer_iterations + + +func seed_value() -> int: + return _attribute.test_seed + + +func is_fuzzed() -> bool: + return not _attribute.fuzzers.is_empty() + + +func fuzzer_arguments() -> Array[GdFunctionArgument]: + return _attribute.fuzzers + + +func script_path() -> String: + return _test_case.source_file + + +func ResourcePath() -> String: + return _test_case.source_file + + +func generate_seed() -> void: + if _attribute.test_seed != -1: + seed(_attribute.test_seed) + + +func do_skip(skipped: bool, reason: String="") -> void: + _attribute.is_skipped = skipped + _attribute.skip_reason = reason + + +func set_function_descriptor(fd: GdFunctionDescriptor) -> void: + _parameter_set_resolver = GdUnitTestParameterSetResolver.new(fd) + + +func _to_string() -> String: + return "%s :%d (%dms)" % [get_name(), _test_case.line_number, _attribute.timeout] diff --git a/addons/gdUnit4/src/core/_TestCase.gd.uid b/addons/gdUnit4/src/core/_TestCase.gd.uid new file mode 100644 index 0000000..0245b6b --- /dev/null +++ b/addons/gdUnit4/src/core/_TestCase.gd.uid @@ -0,0 +1 @@ +uid://dj4m34rrerwxc diff --git a/addons/gdUnit4/src/core/assets/touch-button.png b/addons/gdUnit4/src/core/assets/touch-button.png new file mode 100644 index 0000000..23f46ef Binary files /dev/null and b/addons/gdUnit4/src/core/assets/touch-button.png differ diff --git a/addons/gdUnit4/src/core/assets/touch-button.png.import b/addons/gdUnit4/src/core/assets/touch-button.png.import new file mode 100644 index 0000000..57624bf --- /dev/null +++ b/addons/gdUnit4/src/core/assets/touch-button.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cpeb7ghihwel7" +path="res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/core/assets/touch-button.png" +dest_files=["res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd new file mode 100644 index 0000000..cf7a2b9 --- /dev/null +++ b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd @@ -0,0 +1,76 @@ +class_name TestCaseAttribute +extends Resource +## Holds configuration and metadata for individual test cases.[br] +## [br] +## This class defines test behaviors and properties such as:[br] +## - Test timeouts[br] +## - Skip conditions[br] +## - Fuzzing parameters[br] +## - Random seed values[br] + + +## When set, no specific timeout value is configured and test will use the [code]test_timeout[/code][br] +## value from [GdUnitSettings]. +const DEFAULT_TIMEOUT := -1 + + +## The maximum time in milliseconds for test completion.[br] +## The test fails if execution exceeds this duration.[br] +## [br] +## When set to [constant DEFAULT_TIMEOUT], uses the value from [method GdUnitSettings.test_timeout]. +var timeout: int = DEFAULT_TIMEOUT: + set(value): + timeout = value + get: + if timeout == DEFAULT_TIMEOUT: + # get the default timeout from the settings + timeout = GdUnitSettings.test_timeout() + return timeout + + +## The seed used for random number generation in the test.[br] +## Ensures reproducible results for randomized test scenarios.[br] +## A value of -1 indicates no specific seed is set. +var test_seed: int = -1 + + +## Controls whether this test should be skipped during execution.[br] +## Useful for temporarily disabling tests without removing them. +var is_skipped := false + + +## Documents why the test is being skipped.[br] +## [br] +## Should explain the reason for skipping and ideally include:[br] +## - Why the test was disabled[br] +## - Under what conditions it should be re-enabled[br] +## - Any related issues or tickets +var skip_reason := "Unknown" + + +## Number of iterations to run when using fuzzers.[br] +## [br] +## Fuzzers generate random test data to help find edge cases.[br] +## Higher values provide better coverage but increase test duration. +var fuzzer_iterations: int = Fuzzer.ITERATION_DEFAULT_COUNT + + +## Array of fuzzer configurations for test parameters.[br] +## [br] +## Each [GdFunctionArgument] defines how random test data[br] +## should be generated for a particular parameter. +var fuzzers: Array[GdFunctionArgument] = [] + + +# There is a bug in `duplicate` see https://github.com/godotengine/godot/issues/98644 +# we need in addition to overwrite default values with the source values +@warning_ignore("native_method_override") +func clone() -> Resource: + var copy: TestCaseAttribute = TestCaseAttribute.new() + copy.timeout = timeout + copy.test_seed = test_seed + copy.is_skipped = is_skipped + copy.skip_reason = skip_reason + copy.fuzzer_iterations = fuzzer_iterations + copy.fuzzers = fuzzers.duplicate() + return copy diff --git a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid new file mode 100644 index 0000000..2a6da0c --- /dev/null +++ b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid @@ -0,0 +1 @@ +uid://bjqelwgu16f2s diff --git a/addons/gdUnit4/src/core/command/GdUnitBaseCommand.gd b/addons/gdUnit4/src/core/command/GdUnitBaseCommand.gd new file mode 100644 index 0000000..f5c5ffb --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitBaseCommand.gd @@ -0,0 +1,64 @@ +@abstract class_name GdUnitBaseCommand +extends Node + + +var id: String +var icon: Texture2D +var shortcut: Shortcut = null +var shortcut_type: GdUnitShortcut.ShortCut + + +func _init(p_id: String, p_shortcut: GdUnitShortcut.ShortCut = GdUnitShortcut.ShortCut.NONE) -> void: + id = p_id + shortcut_type = p_shortcut + _set_shortcut() + + +func _shortcut_input(event: InputEvent) -> void: + if is_running(): + return + + if shortcut and shortcut.matches_event(event): + execute() + get_viewport().set_input_as_handled() + + +func update_shortcut() -> void: + _set_shortcut() + + +func _set_shortcut() -> void: + if shortcut_type == GdUnitShortcut.ShortCut.NONE: + return + + var property_name := GdUnitShortcut.as_property(shortcut_type) + var property := GdUnitSettings.get_property(property_name) + var keys := GdUnitShortcut.default_keys(shortcut_type) + if property != null: + keys = property.value() + var inputEvent := _create_shortcut_input_even(keys) + + shortcut = Shortcut.new() + shortcut.set_events([inputEvent]) + + +func _create_shortcut_input_even(key_codes: PackedInt32Array) -> InputEventKey: + var inputEvent := InputEventKey.new() + inputEvent.pressed = true + for key_code in key_codes: + match key_code: + KEY_ALT: + inputEvent.alt_pressed = true + KEY_SHIFT: + inputEvent.shift_pressed = true + KEY_CTRL: + inputEvent.ctrl_pressed = true + _: + inputEvent.keycode = key_code as Key + inputEvent.physical_keycode = key_code as Key + return inputEvent + + +@abstract func is_running() -> bool + +@abstract func execute(...parameters: Array) -> void diff --git a/addons/gdUnit4/src/core/command/GdUnitBaseCommand.gd.uid b/addons/gdUnit4/src/core/command/GdUnitBaseCommand.gd.uid new file mode 100644 index 0000000..684528a --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitBaseCommand.gd.uid @@ -0,0 +1 @@ +uid://dero7uw57t8r8 diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandFileSystem.gd b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystem.gd new file mode 100644 index 0000000..6a173c5 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystem.gd @@ -0,0 +1,42 @@ +@abstract class_name GdUnitCommandFileSystem +extends GdUnitBaseCommand + + +var _test_session_command: GdUnitCommandTestSession + +func _init(p_id: String, p_shortcut: GdUnitShortcut.ShortCut, test_session_command: GdUnitCommandTestSession) -> void: + super(p_id, p_shortcut) + _test_session_command = test_session_command + + +func is_running() -> bool: + return _test_session_command.is_running() + + +func execute_tests(paths: PackedStringArray, with_debug: bool) -> void: + var suite_scaner := GdUnitTestSuiteScanner.new() + var scripts: Array[Script] + + for resource_path in paths: + # directories and test-suites are valid to enable the menu + if DirAccess.dir_exists_absolute(resource_path): + scripts.append_array(suite_scaner.scan_directory(resource_path)) + continue + + var file_type := resource_path.get_extension() + if file_type == "gd" or file_type == "cs": + var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path) + + if GdUnitTestSuiteScanner.is_test_suite(script): + scripts.append(script) + + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + var tests_to_execute: Array[GdUnitTestCase] = [] + for script in scripts: + GdUnitTestDiscoverer.discover_tests(script, func(test_case: GdUnitTestCase) -> void: + tests_to_execute.append(test_case) + GdUnitTestDiscoverSink.discover(test_case) + ) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute) + _test_session_command.execute(tests_to_execute, with_debug) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandFileSystem.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystem.gd.uid new file mode 100644 index 0000000..838ee34 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystem.gd.uid @@ -0,0 +1 @@ +uid://cafdyueb5eqwd diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemDebugTests.gd b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemDebugTests.gd new file mode 100644 index 0000000..c4c553c --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemDebugTests.gd @@ -0,0 +1,17 @@ +class_name GdUnitCommandFileSystemDebugTests +extends GdUnitCommandFileSystem + + +const ID := "Debug FileSystem Tests" + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG, test_session_command) + icon = GdUnitUiTools.get_icon("PlayStart") + + +func execute(...parameters: Array) -> void: + if parameters.is_empty(): + return + var selected_paths: PackedStringArray = parameters[0] + execute_tests(selected_paths, true) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemDebugTests.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemDebugTests.gd.uid new file mode 100644 index 0000000..8cdf819 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemDebugTests.gd.uid @@ -0,0 +1 @@ +uid://dgwik8is03nea diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemRunTests.gd b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemRunTests.gd new file mode 100644 index 0000000..d17413f --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemRunTests.gd @@ -0,0 +1,15 @@ +class_name GdUnitCommandFileSystemRunTests +extends GdUnitCommandFileSystem + + +const ID := "Run FileSystem Tests" + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RUN_TESTSUITE, test_session_command) + icon = GdUnitUiTools.get_icon("Play") + + +func execute(...parameters: Array) -> void: + var selected_paths: PackedStringArray = parameters[0] + execute_tests(selected_paths, false) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemRunTests.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemRunTests.gd.uid new file mode 100644 index 0000000..b595d25 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandFileSystemRunTests.gd.uid @@ -0,0 +1 @@ +uid://6ly3jod2wgim diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd new file mode 100644 index 0000000..78c38ce --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -0,0 +1,126 @@ +class_name GdUnitCommandHandler +extends Object + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _commnand_mappings: Dictionary[String, GdUnitBaseCommand]= {} +var test_session_command := GdUnitCommandTestSession.new() + +static func instance() -> GdUnitCommandHandler: + return GdUnitSingleton.instance("GdUnitCommandHandler", func() -> GdUnitCommandHandler: return GdUnitCommandHandler.new()) + + +@warning_ignore("return_value_discarded") +func _init() -> void: + GdUnitSignals.instance().gdunit_event.connect(_on_event) + GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) + + _register_command(test_session_command) + _register_command(GdUnitCommandStopTestSession.new(test_session_command)) + _register_command(GdUnitCommandInspectorRunTests.new(test_session_command)) + _register_command(GdUnitCommandInspectorDebugTests.new(test_session_command)) + _register_command(GdUnitCommandInspectorRerunTestsUntilFailure.new(test_session_command)) + _register_command(GdUnitCommandInspectorTreeCollapse.new()) + _register_command(GdUnitCommandInspectorTreeExpand.new()) + _register_command(GdUnitCommandScriptEditorRunTests.new(test_session_command)) + _register_command(GdUnitCommandScriptEditorDebugTests.new(test_session_command)) + _register_command(GdUnitCommandScriptEditorCreateTest.new()) + _register_command(GdUnitCommandFileSystemRunTests.new(test_session_command)) + _register_command(GdUnitCommandFileSystemDebugTests.new(test_session_command)) + _register_command(GdUnitCommandRunTestsOverall.new(test_session_command)) + + # schedule discover tests if enabled and running inside the editor + if Engine.is_editor_hint() and GdUnitSettings.is_test_discover_enabled(): + var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(5) + @warning_ignore("return_value_discarded") + timer.timeout.connect(cmd_discover_tests) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + for command: GdUnitBaseCommand in _commnand_mappings.values(): + if Engine.is_editor_hint(): + EditorInterface.get_command_palette().remove_command("GdUnit4/"+command.id) + command.free() + _commnand_mappings.clear() + + +func _do_process() -> void: + # Do stop test execution when the user has stoped the test runner manually by hit the Godot editor stop button + if test_session_command._is_debug and test_session_command.is_running() and not EditorInterface.is_playing_scene(): + if GdUnitSettings.is_verbose_assert_warnings(): + print_debug("Test Runner scene was stopped manually, force stopping the current test run!") + command_execute(GdUnitCommandStopTestSession.ID) + + +func command_icon(command_id: String) -> Texture2D: + if not _commnand_mappings.has(command_id): + push_error("GdUnitCommandHandler:command_icon(): No command id '%s' is registered." % command_id) + print_stack() + return + return _commnand_mappings[command_id].icon + + +func command_shortcut(command_id: String) -> Shortcut: + if not _commnand_mappings.has(command_id): + push_error("GdUnitCommandHandler:command_shortcut(): No command id '%s' is registered." % command_id) + print_stack() + return + return _commnand_mappings[command_id].shortcut + + +func command_execute(...parameters: Array) -> void: + if parameters.is_empty(): + push_error("Invalid arguments used on CommandHandler:execute()! Expecting []") + print_stack() + return + + var command_id: String = parameters.pop_front() + if not _commnand_mappings.has(command_id): + push_error("GdUnitCommandHandler:command_execute(): No command id '%s' is registered." % command_id) + print_stack() + return + await _commnand_mappings[command_id].callv("execute", parameters) + + +func _register_command(command: GdUnitBaseCommand) -> void: + # first verify the command is not already registerd + if _commnand_mappings.has(command.id): + push_error("GdUnitCommandHandler:_register_command(): Command with id '%s' is already registerd!" % command.id) + return + + _commnand_mappings[command.id] = command + if Engine.is_editor_hint(): + EditorInterface.get_base_control().add_child(command) + EditorInterface.get_command_palette().add_command(command.id, "GdUnit4/"+command.id, command.execute, command.shortcut.get_as_text() if command.shortcut else "None") + + +func cmd_discover_tests() -> void: + await GdUnitTestDiscoverer.run() + + +################################################################################ +# signals handles +################################################################################ +func _on_event(event: GdUnitEvent) -> void: + if event.type() == GdUnitEvent.SESSION_CLOSE: + command_execute(GdUnitCommandStopTestSession.ID) + + +func _on_settings_changed(property: GdUnitProperty) -> void: + for command: GdUnitBaseCommand in _commnand_mappings.values(): + command.update_shortcut() + + if property.name() == GdUnitSettings.TEST_DISCOVER_ENABLED: + var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(3) + @warning_ignore("return_value_discarded") + timer.timeout.connect(cmd_discover_tests) + + +################################################################################ +# Network stuff +################################################################################ +func _on_client_disconnected(_client_id: int) -> void: + command_execute(GdUnitCommandStopTestSession.ID) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid new file mode 100644 index 0000000..ae66965 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid @@ -0,0 +1 @@ +uid://bmf7eqd8tp4c0 diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorDebugTests.gd b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorDebugTests.gd new file mode 100644 index 0000000..0c9198f --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorDebugTests.gd @@ -0,0 +1,27 @@ +class_name GdUnitCommandInspectorDebugTests +extends GdUnitBaseCommand + +const GdUnitInspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd") +const ID := "Debug Inspector Tests" + + +var _test_session_command: GdUnitCommandTestSession + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG) + icon = GdUnitUiTools.get_icon("PlayStart") + _test_session_command = test_session_command + + +func is_running() -> bool: + return _test_session_command.is_running() + + +func execute(..._parameters: Array) -> void: + var base_control := EditorInterface.get_base_control() + var inspector: GdUnitInspectorTreeMainPanel = base_control.get_meta("GdUnit4Inspector") + var selected_item := inspector._tree.get_selected() + var tests_to_execute := inspector.collect_test_cases(selected_item) + + _test_session_command.execute(tests_to_execute, true) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorDebugTests.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorDebugTests.gd.uid new file mode 100644 index 0000000..92457ca --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorDebugTests.gd.uid @@ -0,0 +1 @@ +uid://bs4nisyybmfgb diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRerunTestsUntilFailure.gd b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRerunTestsUntilFailure.gd new file mode 100644 index 0000000..9a11c86 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRerunTestsUntilFailure.gd @@ -0,0 +1,57 @@ +class_name GdUnitCommandInspectorRerunTestsUntilFailure +extends GdUnitBaseCommand + + +signal session_closed() + + +const GdUnitInspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd") +const ID := "Rerun Inspector Tests Until Failure" + + +var _test_session_command: GdUnitCommandTestSession +var _current_execution_count := 0 + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RERUN_TESTS_UNTIL_FAILURE) + icon = GdUnitUiTools.get_icon("Play") + _test_session_command = test_session_command + + +func is_running() -> bool: + return _test_session_command.is_running() + + +func execute(..._parameters: Array) -> void: + var base_control := EditorInterface.get_base_control() + var inspector: GdUnitInspectorTreeMainPanel = base_control.get_meta("GdUnit4Inspector") + var selected_item := inspector._tree.get_selected() + var tests_to_execute := inspector.collect_test_cases(selected_item) + var rerun_until_failure_count := GdUnitSettings.get_rerun_max_retries() + var saved_settings: bool = ProjectSettings.get_setting(GdUnitSettings.TEST_FLAKY_CHECK) + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_CHECK, false) + + GdUnitSignals.instance().gdunit_event.connect(_on_test_event) + _current_execution_count = 0 + + _test_session_command._is_fail_fast = true + while _current_execution_count < rerun_until_failure_count: + _test_session_command.execute(tests_to_execute, true) + await session_closed + _test_session_command._is_fail_fast = false + + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_CHECK, saved_settings) + GdUnitSignals.instance().gdunit_event.disconnect(_on_test_event) + + +func _on_test_event(event: GdUnitEvent) -> void: + if event.type() == GdUnitEvent.SESSION_START: + _current_execution_count += 1 + GdUnitSignals.instance().gdunit_message.emit("[color=RED]Execution Mode: ReRun until failure! (iteration %d)[/color]" % _current_execution_count) + if event.type() == GdUnitEvent.SESSION_CLOSE: + session_closed.emit() + if event.type() == GdUnitEvent.TESTCASE_AFTER: + if not event.is_success(): + GdUnitSignals.instance().gdunit_message.emit(" [color=RED](iteration: %d)[/color]" % _current_execution_count) + _current_execution_count = 9999 diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRerunTestsUntilFailure.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRerunTestsUntilFailure.gd.uid new file mode 100644 index 0000000..2186e3b --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRerunTestsUntilFailure.gd.uid @@ -0,0 +1 @@ +uid://di6yeerp8o1es diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRunTests.gd b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRunTests.gd new file mode 100644 index 0000000..436f171 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRunTests.gd @@ -0,0 +1,26 @@ +class_name GdUnitCommandInspectorRunTests +extends GdUnitBaseCommand + +const GdUnitInspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd") +const ID := "Run Inspector Tests" + + +var _test_session_command: GdUnitCommandTestSession + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RERUN_TESTS) + icon = GdUnitUiTools.get_icon("Play") + _test_session_command = test_session_command + + +func is_running() -> bool: + return _test_session_command.is_running() + + +func execute(..._parameters: Array) -> void: + var base_control := EditorInterface.get_base_control() + var inspector: GdUnitInspectorTreeMainPanel = base_control.get_meta("GdUnit4Inspector") + var selected_item := inspector._tree.get_selected() + var tests_to_execute := inspector.collect_test_cases(selected_item) + _test_session_command.execute(tests_to_execute, false) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRunTests.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRunTests.gd.uid new file mode 100644 index 0000000..370a2e4 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorRunTests.gd.uid @@ -0,0 +1 @@ +uid://kjqk4g4dbg7a diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeCollapse.gd b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeCollapse.gd new file mode 100644 index 0000000..f155fb3 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeCollapse.gd @@ -0,0 +1,26 @@ +class_name GdUnitCommandInspectorTreeCollapse +extends GdUnitBaseCommand + +const GdUnitInspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd") +const ID := "Inspector Tree Collapse" + + +func _init() -> void: + super(ID, GdUnitShortcut.ShortCut.NONE) + icon = GdUnitUiTools.get_icon("CollapseTree") + + +func is_running() -> bool: + return false + + +func execute(..._parameters: Array) -> void: + var inspector: GdUnitInspectorTreeMainPanel = EditorInterface.get_base_control().get_meta("GdUnit4Inspector") + var selected_item := inspector._tree.get_selected() + if selected_item == null: + selected_item = inspector._tree.get_root() + else: + selected_item = selected_item.get_parent() + + inspector.do_collapse_all(false, selected_item) + inspector.do_collapse_all(true, selected_item) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeCollapse.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeCollapse.gd.uid new file mode 100644 index 0000000..32936ba --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeCollapse.gd.uid @@ -0,0 +1 @@ +uid://bqp8m0mwg4vmn diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeExpand.gd b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeExpand.gd new file mode 100644 index 0000000..b5754fe --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeExpand.gd @@ -0,0 +1,25 @@ +class_name GdUnitCommandInspectorTreeExpand +extends GdUnitBaseCommand + +const GdUnitInspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd") +const ID := "Inspector Tree Expand" + + +func _init() -> void: + super(ID, GdUnitShortcut.ShortCut.NONE) + icon = GdUnitUiTools.get_icon("ExpandTree") + + +func is_running() -> bool: + return false + + +func execute(..._parameters: Array) -> void: + var inspector: GdUnitInspectorTreeMainPanel = EditorInterface.get_base_control().get_meta("GdUnit4Inspector") + var selected_item := inspector._tree.get_selected() + if selected_item == null: + selected_item = inspector._tree.get_root() + else: + selected_item = selected_item.get_parent() + + inspector.do_collapse_all(false, selected_item) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeExpand.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeExpand.gd.uid new file mode 100644 index 0000000..a04f1ed --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandInspectorTreeExpand.gd.uid @@ -0,0 +1 @@ +uid://tnui11dday2g diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandRunTestsOverall.gd b/addons/gdUnit4/src/core/command/GdUnitCommandRunTestsOverall.gd new file mode 100644 index 0000000..daf534b --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandRunTestsOverall.gd @@ -0,0 +1,22 @@ +class_name GdUnitCommandRunTestsOverall +extends GdUnitBaseCommand + +const ID := "Run Tests Overall" + + +var _test_session_command: GdUnitCommandTestSession + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL) + icon = GdUnitUiTools.get_run_overall_icon() + _test_session_command = test_session_command + + +func is_running() -> bool: + return _test_session_command.is_running() + + +func execute(..._parameters: Array) -> void: + var tests_to_execute := await GdUnitTestDiscoverer.run() + _test_session_command.execute(tests_to_execute, true) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandRunTestsOverall.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandRunTestsOverall.gd.uid new file mode 100644 index 0000000..dc01989 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandRunTestsOverall.gd.uid @@ -0,0 +1 @@ +uid://srmo1dgsej1y diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditor.gd b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditor.gd new file mode 100644 index 0000000..5504fa7 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditor.gd @@ -0,0 +1,56 @@ +@abstract class_name GdUnitCommandScriptEditor +extends GdUnitBaseCommand + +var _test_session_command: GdUnitCommandTestSession + +func _init(p_id: String, p_shortcut: GdUnitShortcut.ShortCut, test_session_command: GdUnitCommandTestSession) -> void: + super(p_id, p_shortcut) + _test_session_command = test_session_command + + +func is_running() -> bool: + return _test_session_command.is_running() + + +func execute_tests(with_debug: bool) -> void: + var selected_tests := PackedStringArray() + if _is_active_script_editor(): + var cursor_line := _active_base_editor().get_caret_line() + #run test case? + var regex := RegEx.new() + @warning_ignore("return_value_discarded") + regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") + var result := regex.search(_active_base_editor().get_line(cursor_line)) + if result: + var func_name := result.get_string(2).strip_edges() + if func_name.begins_with("test_"): + selected_tests.append(func_name) + + var tests_to_execute := _collect_tests(_active_script(), selected_tests) + _test_session_command.execute(tests_to_execute, with_debug) + + +func _collect_tests(script: Script, tests: PackedStringArray) -> Array[GdUnitTestCase]: + # Update test discovery + var tests_to_execute: Array[GdUnitTestCase] = [] + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + if tests.is_empty() or tests.has(test.test_name): + tests_to_execute.append(test) + GdUnitTestDiscoverSink.discover(test) + ) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute) + return tests_to_execute + + +func _is_active_script_editor() -> bool: + return EditorInterface.get_script_editor().get_current_editor() != null + + +func _active_base_editor() -> TextEdit: + return EditorInterface.get_script_editor().get_current_editor().get_base_editor() + + +func _active_script() -> Script: + return EditorInterface.get_script_editor().get_current_script() diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditor.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditor.gd.uid new file mode 100644 index 0000000..30c5f70 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditor.gd.uid @@ -0,0 +1 @@ +uid://btb3r46acy22x diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorCreateTest.gd b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorCreateTest.gd new file mode 100644 index 0000000..d2deaed --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorCreateTest.gd @@ -0,0 +1,41 @@ +class_name GdUnitCommandScriptEditorCreateTest +extends GdUnitBaseCommand + + +const ID := "Create Test" + + +func _init() -> void: + super(ID, GdUnitShortcut.ShortCut.CREATE_TEST) + icon = GdUnitUiTools.get_icon("New") + + +func is_running() -> bool: + return false + + +func execute(..._parameters: Array) -> void: + if not _is_active_script_editor(): + return + var cursor_line := _active_base_editor().get_caret_line() + var result := GdUnitTestSuiteBuilder.create(_active_script(), cursor_line) + if result.is_error(): + # show error dialog + push_error("Failed to create test case: %s" % result.error_message()) + return + var info: Dictionary = result.value() + var script_path: String = info.get("path") + var script_line: int = info.get("line") + GdUnitScriptEditorControls.edit_script(script_path, script_line) + + +func _is_active_script_editor() -> bool: + return EditorInterface.get_script_editor().get_current_editor() != null + + +func _active_base_editor() -> TextEdit: + return EditorInterface.get_script_editor().get_current_editor().get_base_editor() + + +func _active_script() -> Script: + return EditorInterface.get_script_editor().get_current_script() diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorCreateTest.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorCreateTest.gd.uid new file mode 100644 index 0000000..51e4af4 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorCreateTest.gd.uid @@ -0,0 +1 @@ +uid://creqsdjnojsiv diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorDebugTests.gd b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorDebugTests.gd new file mode 100644 index 0000000..8ab9f24 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorDebugTests.gd @@ -0,0 +1,14 @@ +class_name GdUnitCommandScriptEditorDebugTests +extends GdUnitCommandScriptEditor + + +const ID := "Debug ScriptEditor Tests" + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG, test_session_command) + icon = GdUnitUiTools.get_icon("PlayStart") + + +func execute(..._parameters: Array) -> void: + execute_tests(true) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorDebugTests.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorDebugTests.gd.uid new file mode 100644 index 0000000..87d220e --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorDebugTests.gd.uid @@ -0,0 +1 @@ +uid://dy804aj54i8xm diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorRunTests.gd b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorRunTests.gd new file mode 100644 index 0000000..a18851a --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorRunTests.gd @@ -0,0 +1,14 @@ +class_name GdUnitCommandScriptEditorRunTests +extends GdUnitCommandScriptEditor + + +const ID := "Run ScriptEditor Tests" + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.RUN_TESTCASE, test_session_command) + icon = GdUnitUiTools.get_icon("Play") + + +func execute(..._parameters: Array) -> void: + execute_tests(false) diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorRunTests.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorRunTests.gd.uid new file mode 100644 index 0000000..283c43a --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandScriptEditorRunTests.gd.uid @@ -0,0 +1 @@ +uid://xdlidghpny75 diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandStopTestSession.gd b/addons/gdUnit4/src/core/command/GdUnitCommandStopTestSession.gd new file mode 100644 index 0000000..50a6df7 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandStopTestSession.gd @@ -0,0 +1,21 @@ +class_name GdUnitCommandStopTestSession +extends GdUnitBaseCommand + +const ID := "Stop Test Session" + + +var _test_session_command: GdUnitCommandTestSession + + +func _init(test_session_command: GdUnitCommandTestSession) -> void: + super(ID, GdUnitShortcut.ShortCut.STOP_TEST_RUN) + icon = GdUnitUiTools.get_icon("Stop") + _test_session_command = test_session_command + + +func is_running() -> bool: + return _test_session_command.is_running() + + +func execute(..._parameters: Array) -> void: + await _test_session_command.stop() diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandStopTestSession.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandStopTestSession.gd.uid new file mode 100644 index 0000000..5ec11b5 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandStopTestSession.gd.uid @@ -0,0 +1 @@ +uid://bd3dnx4gx3idj diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandTestSession.gd b/addons/gdUnit4/src/core/command/GdUnitCommandTestSession.gd new file mode 100644 index 0000000..e87b7b4 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandTestSession.gd @@ -0,0 +1,122 @@ +class_name GdUnitCommandTestSession +extends GdUnitBaseCommand + + +const ID := "Start Test Session" + + +var _current_runner_process_id: int +var _is_running: bool +var _is_debug: bool +var _is_fail_fast: bool + + +func _init() -> void: + super(ID, GdUnitShortcut.ShortCut.NONE) + _is_running = false + _is_fail_fast = false + + +func is_running() -> bool: + return _is_running + + +func stop() -> void: + if not is_running(): + return + _is_running = false + + if _is_debug: + force_pause_scene() + + GdUnitSignals.instance().gdunit_test_session_terminate.emit() + # Give the API time to commit terminate to the client + await get_tree().create_timer(.5).timeout + + if _is_debug and EditorInterface.is_playing_scene(): + EditorInterface.stop_playing_scene() + elif OS.is_process_running(_current_runner_process_id): + var result := OS.kill(_current_runner_process_id) + if result != OK: + push_error("ERROR checked stopping GdUnit Test Runner. error code: %s" % result) + _current_runner_process_id = -1 + # We need finaly to send the test session close event because the current run is hard aborted. + GdUnitSignals.instance().gdunit_event.emit(GdUnitSessionClose.new()) + + +## Forces the running scene to unpause when the debugger hits a breakpoint.[br] +## [br] +## When the Godot debugger stops at a breakpoint during test execution, it blocks[br] +## the main thread. This prevents signals and TCP communications from being processed,[br] +## which can cause GdUnit4 tests to hang or fail to communicate properly with the[br] +## test runner. This function programmatically unpauses the scene to restore[br] +## main thread execution while maintaining debugger functionality. [br] +## [br] +## [b]Technical Background:[/b][br] +## - Debugger breakpoints freeze the main thread to allow inspection[br] +## - Frozen main thread blocks signal processing and network communications[br] +## - GdUnit4 requires active signal/TCP processing for test coordination[br] +## - This function finds and triggers the editor's pause button to resume execution[br] +## [br] +## [b]How It Works:[/b][br] +## 1. Locates the EditorRunBar in the Godot editor UI hierarchy[br] +## 2. Searches for the pause button by matching its icon[br] +## 3. Unpresses the button if it's currently pressed (paused state)[br] +## 4. Manually triggers the button's connected callbacks to resume execution[br] +func force_pause_scene() -> bool: + var nodes := EditorInterface.get_base_control().find_children("*", "EditorRunBar", true, false) + if nodes.size() != 1: + push_error("GdUnitCommandTestSession:force_pause_scene() Can't find Editor component 'EditorRunBar'") + return false + var editor_run_bar := nodes[0] + var containers := editor_run_bar.find_children("*", "HBoxContainer", true, false) + var pause_icon := GdUnitUiTools.get_icon("Pause") + + for container in containers: + for child in container.get_children(): + if child is Button: + var button: Button = child + if pause_icon == button.icon: + button.set_pressed(false) + + var connected_signals := button.get_signal_connection_list("pressed") + if not connected_signals.is_empty(): + for signal_ in connected_signals: + var cb: Callable = signal_["callable"] + cb.call() + return true + push_error("GdUnitCommandTestSession:force_pause_scene() Can't find Editor component 'EditorRunBar'") + return false + + +func execute(...parameters: Array) -> void: + var tests_to_execute: Array[GdUnitTestCase] = parameters[0] + _is_debug = parameters[1] + + _prepare_test_session(tests_to_execute) + if _is_debug: + EditorInterface.play_custom_scene("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn") + else: + var arguments := Array() + if OS.is_stdout_verbose(): + arguments.append("--verbose") + arguments.append("--no-window") + arguments.append("--path") + arguments.append(ProjectSettings.globalize_path("res://")) + arguments.append("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn") + _current_runner_process_id = OS.create_process(OS.get_executable_path(), arguments, false); + _is_running = true + + +func _prepare_test_session(tests_to_execute: Array[GdUnitTestCase]) -> void: + var server_port: int = Engine.get_meta("gdunit_server_port") + var result := GdUnitRunnerConfig.new() \ + .set_server_port(server_port) \ + .do_fail_fast(_is_fail_fast) \ + .add_test_cases(tests_to_execute) \ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + # before start we have to save all scrpt changes + GdUnitScriptEditorControls.save_all_open_script() diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandTestSession.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandTestSession.gd.uid new file mode 100644 index 0000000..92120ec --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandTestSession.gd.uid @@ -0,0 +1 @@ +uid://blg8qcvk56baf diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd new file mode 100644 index 0000000..33af082 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd @@ -0,0 +1,73 @@ +class_name GdUnitShortcut +extends RefCounted + + +enum ShortCut { + NONE, + RUN_TESTS_OVERALL, + RUN_TESTCASE, + RUN_TESTCASE_DEBUG, + RUN_TESTSUITE, + RUN_TESTSUITE_DEBUG, + RERUN_TESTS, + RERUN_TESTS_DEBUG, + RERUN_TESTS_UNTIL_FAILURE, + STOP_TEST_RUN, + CREATE_TEST, +} + +const DEFAULTS_MACOS := { + ShortCut.NONE : [], + ShortCut.RUN_TESTCASE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTSUITE : [Key.KEY_ALT, Key.KEY_META, Key.KEY_F5], + ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_ALT, Key.KEY_META, Key.KEY_F6], + ShortCut.RUN_TESTS_OVERALL : [Key.KEY_ALT, Key.KEY_F7], + ShortCut.STOP_TEST_RUN : [Key.KEY_ALT, Key.KEY_F8], + ShortCut.RERUN_TESTS : [Key.KEY_ALT, Key.KEY_F5], + ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_ALT, Key.KEY_F6], + ShortCut.RERUN_TESTS_UNTIL_FAILURE : [Key.KEY_ALT, Key.KEY_META, Key.KEY_F5], + ShortCut.CREATE_TEST : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F10], +} + +const DEFAULTS_WINDOWS := { + ShortCut.NONE : [], + ShortCut.RUN_TESTCASE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTSUITE : [Key.KEY_ALT, Key.KEY_SHIFT, Key.KEY_F5], + ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_ALT, Key.KEY_SHIFT, Key.KEY_F6], + ShortCut.RUN_TESTS_OVERALL : [Key.KEY_ALT, Key.KEY_F7], + ShortCut.STOP_TEST_RUN : [Key.KEY_ALT, Key.KEY_F8], + ShortCut.RERUN_TESTS : [Key.KEY_ALT, Key.KEY_F5], + ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_ALT, Key.KEY_F6], + ShortCut.RERUN_TESTS_UNTIL_FAILURE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5], + ShortCut.CREATE_TEST : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F10], +} + + +const SETTINGS_MAPPING: Dictionary[ShortCut, String] = { + ShortCut.RUN_TESTCASE : GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST, + ShortCut.RUN_TESTCASE_DEBUG : GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST_DEBUG, + ShortCut.CREATE_TEST : GdUnitSettings.SHORTCUT_EDITOR_CREATE_TEST, + ShortCut.RERUN_TESTS : GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST, + ShortCut.RERUN_TESTS_DEBUG : GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, + ShortCut.RERUN_TESTS_UNTIL_FAILURE : GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_UNTIL_FAILURE, + ShortCut.STOP_TEST_RUN : GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_STOP, + ShortCut.RUN_TESTS_OVERALL : GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, + ShortCut.RUN_TESTSUITE : GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST, + ShortCut.RUN_TESTSUITE_DEBUG : GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG +} + + +static func as_property(sortcut: ShortCut) -> String: + return SETTINGS_MAPPING[sortcut] + + +static func default_keys(shortcut: ShortCut) -> PackedInt32Array: + match OS.get_name().to_lower(): + 'windows': + return DEFAULTS_WINDOWS[shortcut] + 'macos': + return DEFAULTS_MACOS[shortcut] + _: + return DEFAULTS_WINDOWS[shortcut] diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid new file mode 100644 index 0000000..abb6be0 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid @@ -0,0 +1 @@ +uid://lla2qegdfu3k diff --git a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd new file mode 100644 index 0000000..00332f9 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd @@ -0,0 +1,46 @@ +## A class representing a globally unique identifier for GdUnit test elements. +## Uses random values to generate unique identifiers that can be used +## to track and reference test cases and suites across the test framework. +class_name GdUnitGUID +extends RefCounted + + +## The internal string representation of the GUID. +## Generated using Godot's ResourceUID system when no existing GUID is provided. +var _guid: String + + +## Creates a new GUID instance. +## If no GUID is provided, generates a new one using Godot's ResourceUID system. +func _init(from_guid: String = "") -> void: + if from_guid.is_empty(): + _guid = _generate_guid() + else: + _guid = from_guid + + +## Compares this GUID with another for equality. +## Returns true if both GUIDs represent the same unique identifier. +func equals(other: GdUnitGUID) -> bool: + return other._guid == _guid + + +## Generates a custom GUID using random bytes.[br] +## The format uses 16 random bytes encoded to hex and formatted with hyphens. +static func _generate_guid() -> String: + # Pre-allocate array with exact size needed + var bytes := PackedByteArray() + bytes.resize(16) + + # Fill with random bytes + for i in range(16): + bytes[i] = randi() % 256 + + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return bytes.hex_encode().insert(8, "-").insert(16, "-").insert(24, "-") + + +func _to_string() -> String: + return _guid diff --git a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid new file mode 100644 index 0000000..709527a --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid @@ -0,0 +1 @@ +uid://bqpesqpfqq7md diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd new file mode 100644 index 0000000..766f9ea --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd @@ -0,0 +1,125 @@ +## GdUnitTestCase +## A class representing a single test case in GdUnit4. +## This class is used as a data container to hold all relevant information about a test case, +## including its location, dependencies, and metadata for test discovery and execution. + +class_name GdUnitTestCase +extends RefCounted + +## A unique identifier for the test case. Used to track and reference specific test instances. +var guid := GdUnitGUID.new() + +## The resource path to the test suite +var suite_resource_path: String + +## The name of the test method/function. Should start with "test_" prefix. +var test_name: String + +## The class name of the test suite containing this test case. +var suite_name: String + +## The fully qualified name of the test case following C# namespace pattern: +## Constructed from the folder path (where folders are dot-separated), the test suite name, and the test case name. +## All parts are joined by dots: {folder1.folder2.folder3}.{suite_name}.{test_name} +var fully_qualified_name: String + +var display_name: String + +## Index tracking test attributes for ordered execution. Default is 0. +## Higher values indicate later execution in the test sequence. +var attribute_index: int + +## Flag indicating if this test requires the Godot runtime environment. +## Tests requiring runtime cannot be executed in isolation. +var require_godot_runtime: bool = true + +## The path to the source file containing this test case. +## Used for test discovery and execution. +var source_file: String + +## Optional holds the assembly location for C# tests +var assembly_location: String = "" + +## The line number where the test case is defined in the source file. +## Used for navigation and error reporting. +var line_number: int = -1 + +## Additional metadata about the test case, such as: +## - tags: Array[String] - Test categories/tags for filtering +## - timeout: int - Maximum execution time in milliseconds +## - skip: bool - Whether the test should be skipped +## - dependencies: Array[String] - Required test dependencies +var metadata: Dictionary = {} + + +static func from_dict(dict: Dictionary) -> GdUnitTestCase: + var test := GdUnitTestCase.new() + test.guid = GdUnitGUID.new(str(dict["guid"])) + test.suite_resource_path = dict["suite_resource_path"] if dict.has("suite_resource_path") else dict["source_file"] + test.suite_name = dict["managed_type"] + test.test_name = dict["test_name"] + test.display_name = dict["simple_name"] + test.fully_qualified_name = dict["fully_qualified_name"] + test.attribute_index = dict["attribute_index"] + test.source_file = dict["source_file"] + test.line_number = dict["line_number"] + test.require_godot_runtime = dict["require_godot_runtime"] + test.assembly_location = dict["assembly_location"] + return test + + +static func to_dict(test: GdUnitTestCase) -> Dictionary: + return { + "guid": test.guid._guid, + "suite_resource_path": test.suite_resource_path, + "managed_type": test.suite_name, + "test_name" : test.test_name, + "simple_name" : test.display_name, + "fully_qualified_name" : test.fully_qualified_name, + "attribute_index" : test.attribute_index, + "source_file" : test.source_file, + "line_number" : test.line_number, + "require_godot_runtime" : test.require_godot_runtime, + "assembly_location" : test.assembly_location + } + + +static func from(_suite_resource_path: String, _source_file: String, _line_number: int, _test_name: String, _attribute_index := -1, _test_parameters := "") -> GdUnitTestCase: + if(_source_file == null or _source_file.is_empty()): + prints(_test_name) + + assert(_test_name != null and not _test_name.is_empty(), "Precondition: The parameter 'test_name' is not set") + assert(_source_file != null and not _source_file.is_empty(), "Precondition: The parameter 'source_file' is not set") + + var test := GdUnitTestCase.new() + test.suite_resource_path = _suite_resource_path + test.test_name = _test_name + test.source_file = _source_file + test.line_number = _line_number + test.attribute_index = _attribute_index + test._build_suite_name() + test._build_display_name(_test_parameters) + test._build_fully_qualified_name(_suite_resource_path) + return test + + +func _build_suite_name() -> void: + suite_name = source_file.get_file().get_basename() + assert(suite_name != null and not suite_name.is_empty(), "Precondition: The parameter 'suite_name' can't be resolved") + + +func _build_display_name(_test_parameters: String) -> void: + if attribute_index == -1: + display_name = test_name + else: + display_name = "%s:%d (%s)" % [test_name, attribute_index, _test_parameters.trim_prefix("[").trim_suffix("]").replace('"', "'")] + + +func _build_fully_qualified_name(_resource_path: String) -> void: + var name_space := _resource_path.trim_prefix("res://").trim_suffix(".gd").trim_suffix(".cs").replace("/", ".") + + if attribute_index == -1: + fully_qualified_name = "%s.%s" % [name_space, test_name] + else: + fully_qualified_name = "%s.%s.%s" % [name_space, test_name, display_name] + assert(fully_qualified_name != null and not fully_qualified_name.is_empty(), "Precondition: The parameter 'fully_qualified_name' can't be resolved") diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid new file mode 100644 index 0000000..96d259e --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid @@ -0,0 +1 @@ +uid://u15fmjnk3blp diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd new file mode 100644 index 0000000..6af8255 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd @@ -0,0 +1,323 @@ +## Guards and tracks test case changes during test discovery and file modifications.[br] +## [br] +## This guard maintains a cache of discovered tests to track changes between test runs and during[br] +## file modifications. It is optimized for performance using simple but effective test identity checks.[br] +## [br] +## Test Change Detection:[br] +## - Moved tests: The test implementation remains at a different line number[br] +## - Renamed tests: The test line position remains but the test name changed[br] +## - Deleted tests: A previously discovered test was removed[br] +## - Added tests: A new test was discovered[br] +## [br] +## Cache Management:[br] +## - Maintains test identity through unique GdUnitTestCase GUIDs[br] +## - Maps source files to their discovered test cases[br] +## - Tracks only essential metadata (line numbers, names) to minimize memory use[br] +## [br] +## Change Detection Strategy:[br] +## The guard uses a lightweight approach by comparing only line numbers and test names.[br] +## This avoids expensive operations like test content parsing or similarity checks.[br] +## [br] +## Event Handling:[br] +## - Emits events on test changes through GdUnitSignals[br] +## - Synchronizes cache with test discovery events[br] +## - Notifies UI about test changes[br] +## [br] +## Example usage:[br] +## [codeblock] +## # Create guard for tracking test changes +## var guard := GdUnitTestDiscoverGuard.new() +## +## # Connect to test discovery events +## GdUnitSignals.instance().gdunit_test_discovered.connect(guard.sync_test_added) +## +## # Discover tests and track changes +## await guard.discover(test_script) +## [/codeblock] +class_name GdUnitTestDiscoverGuard +extends Object + + + +static func instance() -> GdUnitTestDiscoverGuard: + return GdUnitSingleton.instance("GdUnitTestDiscoverGuard", func() -> GdUnitTestDiscoverGuard: + return GdUnitTestDiscoverGuard.new() + ) + + +## Maps source files to their discovered test cases.[br] +## [br] +## Key: Test suite source file path[br] +## Value: Array of [class GdUnitTestCase] instances +var _discover_cache := {} + + +## Tracks discovered test changes for debug purposes.[br] +## [br] +## Available in debug mode only. Contains dictionaries:[br] +## - changed_tests: Tests that were moved or renamed[br] +## - deleted_tests: Tests that were removed[br] +## - added_tests: New tests that were discovered +var _discovered_changes := {} + + +## Controls test change debug tracking.[br] +## [br] +## When true, maintains _discovered_changes for debugging.[br] +## Used primarily in tests to verify change detection. +var _is_debug := false + + +## Creates a new guard instance.[br] +## [br] +## [param is_debug] When true, enables change tracking for debugging. +func _init(is_debug := false) -> void: + _is_debug = is_debug + # Register for discovery events to sync the cache + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_test_discover_added.connect(sync_test_added) + GdUnitSignals.instance().gdunit_test_discover_deleted.connect(sync_test_deleted) + GdUnitSignals.instance().gdunit_test_discover_modified.connect(sync_test_modified) + GdUnitSignals.instance().gdunit_event.connect(handle_discover_events) + + +## Adds a discovered test to the cache.[br] +## [br] +## [param test_case] The test case to add to the cache. +func sync_test_added(test_case: GdUnitTestCase) -> void: + var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(test_case.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)) + test_cases.append(test_case) + + +## Removes a test from the cache.[br] +## [br] +## [param test_case] The test case to remove from the cache. +func sync_test_deleted(test_case: GdUnitTestCase) -> void: + var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(test_case.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)) + test_cases.erase(test_case) + + +## Updates a test from the cache.[br] +## [br] +## [param test_case] The test case to update from the cache. +func sync_test_modified(changed_test: GdUnitTestCase) -> void: + var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(changed_test.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)) + for test in test_cases: + if test.guid == changed_test.guid: + test.test_name = changed_test.test_name + test.display_name = changed_test.display_name + test.line_number = changed_test.line_number + break + + +## Handles test discovery events.[br] +## [br] +## Resets the cache when a new discovery starts.[br] +## [param event] The discovery event to handle. +func handle_discover_events(event: GdUnitEvent) -> void: + # reset the cache on fresh discovery + if event.type() == GdUnitEvent.DISCOVER_START: + _discover_cache = {} + + +## Registers a callback for discovered tests.[br] +## [br] +## Default sink writes to [class GdUnitTestDiscoverSink]. +static func default_discover_sink(test_case: GdUnitTestCase) -> void: + GdUnitTestDiscoverSink.discover(test_case) + + +## Finds a test case by its unique identifier.[br] +## [br] +## Searches through all cached test cases across all test suites[br] +## to find a test with the matching GUID.[br] +## [br] +## [param id] The GUID of the test to find[br] +## Returns the matching test case or null if not found. +func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase: + for test_sets: Array[GdUnitTestCase] in _discover_cache.values(): + for test in test_sets: + if test.guid.equals(id): + return test + + return null + + +func get_discovered_tests() -> Array[GdUnitTestCase]: + var discovered_tests: Array[GdUnitTestCase] = [] + for test_sets: Array[GdUnitTestCase] in _discover_cache.values(): + discovered_tests.append_array(test_sets) + return discovered_tests + + +## Discovers tests in a script and tracks changes.[br] +## [br] +## Handles both GDScript and C# test suites.[br] +## The guard maintains test identity through changes.[br] +## [br] +## [param script] The test script to analyze[br] +## [param discover_sink] Optional callback for test discovery events +func discover(script: Script, discover_sink: Callable = default_discover_sink) -> void: + # Verify the script has no errors before run test discovery + var result := script.reload(true) + if result != OK: + return + + if _is_debug: + _discovered_changes["changed_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + _discovered_changes["deleted_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + _discovered_changes["added_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + if GdUnitTestSuiteScanner.is_test_suite(script): + # for cs scripts we need to recomplie before discover new tests + if script.get_class() == "CSharpScript": + await rebuild_project(script) + + # rediscover all tests + var source_file := script.resource_path + var discovered_tests: Array[GdUnitTestCase] = [] + + GdUnitTestDiscoverer.discover_tests(script, func(test_case: GdUnitTestCase) -> void: + discovered_tests.append(test_case) + ) + + # The suite is never discovered, we add all discovered tests + if not _discover_cache.has(source_file): + for test_case in discovered_tests: + discover_sink.call(test_case) + return + + sync_moved_tests(source_file, discovered_tests) + sync_renamed_tests(source_file, discovered_tests) + sync_deleted_tests(source_file, discovered_tests) + sync_added_tests(source_file, discovered_tests, discover_sink) + + +## Synchronizes moved tests between discover cycles.[br] +## [br] +## A test is considered moved when:[br] +## - It has the same name[br] +## - But a different line number[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests +func sync_moved_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + for discovered_test in discovered_tests: + # lookup in cache + var original_tests: Array[GdUnitTestCase] = cache.filter(is_test_moved.bind(discovered_test)) + for test in original_tests: + # update the line_number + var line_number_before := test.line_number + test.line_number = discovered_test.line_number + GdUnitSignals.instance().gdunit_test_discover_modified.emit(test) + if _is_debug: + prints("-> moved test id:%s %s: line:(%d -> %d)" % [test.guid, test.display_name, line_number_before, test.line_number]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("changed_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +## Synchronizes renamed tests between discover cycles.[br] +## [br] +## A test is considered renamed when:[br] +## - It has the same line number[br] +## - But a different name[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests +func sync_renamed_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + for discovered_test in discovered_tests: + # lookup in cache + var original_tests: Array[GdUnitTestCase] = cache.filter(is_test_renamed.bind(discovered_test)) + for test in original_tests: + # update the renaming names + var original_display_name := test.display_name + test.test_name = discovered_test.test_name + test.display_name = discovered_test.display_name + GdUnitSignals.instance().gdunit_test_discover_modified.emit(test) + if _is_debug: + prints("-> renamed test id:%s %s -> %s" % [test.guid, original_display_name, test.display_name]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("changed_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +## Synchronizes deleted tests between discover cycles.[br] +## [br] +## A test is considered deleted when:[br] +## - It exists in the cache[br] +## - But is not found in the newly discovered tests[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests +func sync_deleted_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + # lookup in cache + for test in cache: + if not discovered_tests.any(test_equals.bind(test)): + GdUnitSignals.instance().gdunit_test_discover_deleted.emit(test) + if _is_debug: + prints("-> deleted test id:%s %s:%d" % [test.guid, test.display_name, test.line_number]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("deleted_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +## Synchronizes newly added tests between discover cycles.[br] +## [br] +## A test is considered added when:[br] +## - It exists in the newly discovered tests[br] +## - But is not found in the cache[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests[br] +## [param discover_sink] Callback to handle newly discovered tests +func sync_added_tests(source_file: String, discovered_tests: Array[GdUnitTestCase], discover_sink: Callable) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + # lookup in cache + for test in discovered_tests: + if not cache.any(test_equals.bind(test)): + discover_sink.call(test) + if _is_debug: + prints("-> added test id:%s %s:%d" % [test.guid, test.display_name, test.line_number]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("added_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +func is_test_renamed(left: GdUnitTestCase, right: GdUnitTestCase) -> bool: + return left.line_number == right.line_number and left.test_name != right.test_name + + +func is_test_moved(left: GdUnitTestCase, right: GdUnitTestCase) -> bool: + return left.line_number != right.line_number and left.test_name == right.test_name + + +func test_equals(left: GdUnitTestCase, right: GdUnitTestCase) -> bool: + return left.display_name == right.display_name + + +# do rebuild the entire project, there is actual no way to enforce the Godot engine itself to do this +func rebuild_project(script: Script) -> void: + var class_path := ProjectSettings.globalize_path(script.resource_path) + print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard: CSharpScript change detected on: '%s' [/color]" % class_path) + var scene_tree := Engine.get_main_loop() as SceneTree + await scene_tree.process_frame + + var output := [] + var exit_code := OS.execute("dotnet", ["--version"], output) + if exit_code == -1: + print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Rebuild the project failed.[/color]") + print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Can't find installed `dotnet`! Please check your environment is setup correctly.[/color]") + return + + print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Found dotnet v%s[/color]" % str(output[0]).strip_edges()) + output.clear() + + exit_code = OS.execute("dotnet", ["build"], output) + print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Rebuild the project ... [/color]") + for out: String in output: + print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges()) + await scene_tree.process_frame diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid new file mode 100644 index 0000000..6977057 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid @@ -0,0 +1 @@ +uid://dtoc1qbfhe0e0 diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd new file mode 100644 index 0000000..5d0e5b6 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd @@ -0,0 +1,13 @@ +## A static utility class that acts as a central sink for test case discovery events in GdUnit4. +## Instead of implementing custom sink classes, test discovery consumers should connect to +## the GdUnitSignals.gdunit_test_discovered signal to receive test case discoveries. +## This design allows for a more flexible and decoupled test discovery system. +class_name GdUnitTestDiscoverSink +extends RefCounted + + +## Emits a discovered test case through the GdUnitSignals system.[br] +## Sends the test case to all listeners connected to the gdunit_test_discovered signal.[br] +## [member test_case] The discovered test case to be broadcast to all connected listeners. +static func discover(test_case: GdUnitTestCase) -> void: + GdUnitSignals.instance().gdunit_test_discover_added.emit(test_case) diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid new file mode 100644 index 0000000..575e44d --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid @@ -0,0 +1 @@ +uid://dluusxc36j1p8 diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd new file mode 100644 index 0000000..cb3e407 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd @@ -0,0 +1,171 @@ +class_name GdUnitTestDiscoverer +extends RefCounted + + +static func run() -> Array[GdUnitTestCase]: + console_log("Running test discovery ..") + await (Engine.get_main_loop() as SceneTree).process_frame + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + + # We run the test discovery in an extra thread so that the main thread is not blocked + var t:= Thread.new() + @warning_ignore("return_value_discarded") + t.start(func () -> Array[GdUnitTestCase]: + # Loading previous test session + var runner_config := GdUnitRunnerConfig.new() + runner_config.load_config() + var recovered_tests := runner_config.test_cases() + var test_suite_directories := scan_all_test_directories(GdUnitSettings.test_root_folder()) + var scanner := GdUnitTestSuiteScanner.new() + + var collected_tests: Array[GdUnitTestCase] = [] + var collected_test_suites: Array[Script] = [] + # collect test suites + for test_suite_dir in test_suite_directories: + collected_test_suites.append_array(scanner.scan_directory(test_suite_dir)) + + # Do sync the main thread before emit the discovered test suites to the inspector + await (Engine.get_main_loop() as SceneTree).process_frame + for test_suites_script in collected_test_suites: + discover_tests(test_suites_script, func(test_case: GdUnitTestCase) -> void: + # Sync test uid from last test session + recover_test_guid(test_case, recovered_tests) + collected_tests.append(test_case) + GdUnitTestDiscoverSink.discover(test_case) + ) + + console_log_discover_results(collected_tests) + if !recovered_tests.is_empty(): + console_log("Recovered last test session successfully, %d tests restored." % recovered_tests.size(), true) + return collected_tests + ) + # wait unblocked to the tread is finished + while t.is_alive(): + await (Engine.get_main_loop() as SceneTree).process_frame + # needs finally to wait for finish + var test_to_execute: Array[GdUnitTestCase] = await t.wait_to_finish() + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + return test_to_execute + + +## Restores the last test run session by loading the test run config file and rediscover the tests +static func restore_last_session() -> void: + if GdUnitSettings.is_test_discover_enabled(): + return + + var runner_config := GdUnitRunnerConfig.new() + var result := runner_config.load_config() + # Report possible config loading errors + if result.is_error(): + console_log("Recovery of the last test session failed: %s" % result.error_message(), true) + # If no config file found, skip test recovery + if result.is_warn(): + return + + # If no tests recorded, skip test recovery + var test_cases := runner_config.test_cases() + if test_cases.size() == 0: + return + + # We run the test session restoring in an extra thread so that the main thread is not blocked + var t:= Thread.new() + t.start(func () -> void: + # Do sync the main thread before emit the discovered test suites to the inspector + await (Engine.get_main_loop() as SceneTree).process_frame + console_log("Recovering last test session ..", true) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + for test_case in test_cases: + GdUnitTestDiscoverSink.discover(test_case) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + console_log("Recovered last test session successfully, %d tests restored." % test_cases.size(), true) + ) + t.wait_to_finish() + + +static func recover_test_guid(current: GdUnitTestCase, recovered_tests: Array[GdUnitTestCase]) -> void: + for recovered_test in recovered_tests: + if recovered_test.fully_qualified_name == current.fully_qualified_name: + current.guid = recovered_test.guid + + +static func console_log_discover_results(tests: Array[GdUnitTestCase]) -> void: + var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.source_file + ) + for suite_tests: Array in grouped_by_suites.values(): + var test_case: GdUnitTestCase = suite_tests[0] + console_log("Discover: TestSuite %s with %d tests found" % [test_case.source_file, suite_tests.size()]) + console_log("Discover tests done, %d TestSuites and total %d Tests found. " % [grouped_by_suites.size(), tests.size()]) + console_log("") + + +static func console_log(message: String, on_console := false) -> void: + prints(message) + if on_console: + GdUnitSignals.instance().gdunit_message.emit(message) + + +static func filter_tests(method: Dictionary) -> bool: + var method_name: String = method["name"] + return method_name.begins_with("test_") + + +static func default_discover_sink(test_case: GdUnitTestCase) -> void: + GdUnitTestDiscoverSink.discover(test_case) + + +static func discover_tests(source_script: Script, discover_sink := default_discover_sink) -> void: + if source_script is GDScript: + var test_names := source_script.get_script_method_list()\ + .filter(filter_tests)\ + .map(func(method: Dictionary) -> String: return method["name"]) + # no tests discovered? + if test_names.is_empty(): + return + + var parser := GdScriptParser.new() + var fds := parser.get_function_descriptors(source_script as GDScript, test_names) + for fd in fds: + var resolver := GdFunctionParameterSetResolver.new(fd) + for test_case in resolver.resolve_test_cases(source_script as GDScript): + discover_sink.call(test_case) + elif source_script.get_class() == "CSharpScript": + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return + for test_case in GdUnit4CSharpApiLoader.discover_tests(source_script): + discover_sink.call(test_case) + + +static func scan_all_test_directories(root: String) -> PackedStringArray: + var base_directory := "res://" + # If the test root folder is configured as blank, "/", or "res://", use the root folder as described in the settings panel + if root.is_empty() or root == "/" or root == base_directory: + return [base_directory] + return scan_test_directories(base_directory, root, []) + + +static func scan_test_directories(base_directory: String, test_directory: String, test_suite_paths: PackedStringArray) -> PackedStringArray: + print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory]) + for directory in DirAccess.get_directories_at(base_directory): + if directory.begins_with("."): + continue + var current_directory := normalize_path(base_directory + "/" + directory) + if FileAccess.file_exists(current_directory + "/.gdignore"): + continue + if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): + continue + if match_test_directory(directory, test_directory): + @warning_ignore("return_value_discarded") + test_suite_paths.append(current_directory) + else: + @warning_ignore("return_value_discarded") + scan_test_directories(current_directory, test_directory, test_suite_paths) + return test_suite_paths + + +static func normalize_path(path: String) -> String: + return path.replace("///", "//") + + +static func match_test_directory(directory: String, test_directory: String) -> bool: + return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://" diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid new file mode 100644 index 0000000..bc92710 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid @@ -0,0 +1 @@ +uid://bsqjpkt545gyw diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd b/addons/gdUnit4/src/core/event/GdUnitEvent.gd new file mode 100644 index 0000000..fb5a3a6 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd @@ -0,0 +1,207 @@ +class_name GdUnitEvent +extends Resource + +const WARNINGS = "warnings" +const FAILED = "failed" +const FLAKY = "flaky" +const ERRORS = "errors" +const SKIPPED = "skipped" +const ELAPSED_TIME = "elapsed_time" +const ORPHAN_NODES = "orphan_nodes" +const ERROR_COUNT = "error_count" +const FAILED_COUNT = "failed_count" +const SKIPPED_COUNT = "skipped_count" +const RETRY_COUNT = "retry_count" + +enum { + INIT, + STOP, + TESTSUITE_BEFORE, + TESTSUITE_AFTER, + TESTCASE_BEFORE, + TESTCASE_AFTER, + DISCOVER_START, + DISCOVER_END, + SESSION_START, + SESSION_CLOSE +} + +var _event_type: int +var _guid: GdUnitGUID +var _resource_path: String +var _suite_name: String +var _test_name: String +var _total_count: int = 0 +var _statistics := Dictionary() +var _reports: Array[GdUnitReport] = [] + + +func suite_before(p_resource_path: String, p_suite_name: String, p_total_count: int) -> GdUnitEvent: + _guid = GdUnitGUID.new() + _event_type = TESTSUITE_BEFORE + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = "before" + _total_count = p_total_count + return self + + +func suite_after(p_resource_path: String, p_suite_name: String, p_statistics: Dictionary = {}, p_reports: Array[GdUnitReport] = []) -> GdUnitEvent: + _guid = GdUnitGUID.new() + _event_type = TESTSUITE_AFTER + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = "after" + _statistics = p_statistics + _reports = p_reports + return self + + +func test_before(p_guid: GdUnitGUID) -> GdUnitEvent: + _event_type = TESTCASE_BEFORE + _guid = p_guid + return self + + +func test_after(p_guid: GdUnitGUID, name: String, p_statistics: Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent: + _event_type = TESTCASE_AFTER + _guid = p_guid + _test_name = name + _statistics = p_statistics + _reports = p_reports + return self + + +func type() -> int: + return _event_type + + +func guid() -> GdUnitGUID: + return _guid + + +func suite_name() -> String: + return _suite_name + + +func test_name() -> String: + return _test_name + + +func elapsed_time() -> int: + return _statistics.get(ELAPSED_TIME, 0) + + +func orphan_nodes() -> int: + return _statistics.get(ORPHAN_NODES, 0) + + +func statistic(p_type :String) -> int: + return _statistics.get(p_type, 0) + + +func total_count() -> int: + return _total_count + + +func success_count() -> int: + return total_count() - error_count() - failed_count() - skipped_count() + + +func error_count() -> int: + return _statistics.get(ERROR_COUNT, 0) + + +func failed_count() -> int: + return _statistics.get(FAILED_COUNT, 0) + + +func skipped_count() -> int: + return _statistics.get(SKIPPED_COUNT, 0) + + +func retry_count() -> int: + return _statistics.get(RETRY_COUNT, 0) + + +func resource_path() -> String: + return _resource_path + + +func is_success() -> bool: + return not is_failed() and not is_error() + + +func is_warning() -> bool: + return _statistics.get(WARNINGS, false) or orphan_nodes() > 0 + + +func is_failed() -> bool: + return _statistics.get(FAILED, false) + + +func is_error() -> bool: + return _statistics.get(ERRORS, false) + + +func is_flaky() -> bool: + return _statistics.get(FLAKY, false) + + +func is_skipped() -> bool: + return _statistics.get(SKIPPED, false) + + +func reports() -> Array[GdUnitReport]: + return _reports + + +func _to_string() -> String: + return "Event: %s id:%s %s:%s, %s, %s" % [_event_type, _guid, _suite_name, _test_name, _statistics, _reports] + + +func serialize() -> Dictionary: + var serialized := { + "type" : _event_type, + "resource_path": _resource_path, + "suite_name" : _suite_name, + "test_name" : _test_name, + "total_count" : _total_count, + "statistics" : _statistics + } + if _guid != null: + serialized["guid"] = _guid._guid + serialized["reports"] = _serialize_TestReports() + return serialized + + +func deserialize(serialized: Dictionary) -> GdUnitEvent: + _event_type = serialized.get("type", null) + _guid = GdUnitGUID.new(str(serialized.get("guid", ""))) + _resource_path = serialized.get("resource_path", null) + _suite_name = serialized.get("suite_name", null) + _test_name = serialized.get("test_name", "unknown") + _total_count = serialized.get("total_count", 0) + _statistics = serialized.get("statistics", Dictionary()) + if serialized.has("reports"): + # needs this workaround to copy typed values in the array + var reports_to_deserializ :Array[Dictionary] = [] + @warning_ignore("unsafe_cast") + reports_to_deserializ.append_array(serialized.get("reports") as Array) + _reports = _deserialize_reports(reports_to_deserializ) + return self + + +func _serialize_TestReports() -> Array[Dictionary]: + var serialized_reports :Array[Dictionary] = [] + for report in _reports: + serialized_reports.append(report.serialize()) + return serialized_reports + + +func _deserialize_reports(p_reports: Array[Dictionary]) -> Array[GdUnitReport]: + var deserialized_reports :Array[GdUnitReport] = [] + for report in p_reports: + var test_report := GdUnitReport.new().deserialize(report) + deserialized_reports.append(test_report) + return deserialized_reports diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid new file mode 100644 index 0000000..8140360 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid @@ -0,0 +1 @@ +uid://cn6xlgavfefc8 diff --git a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd new file mode 100644 index 0000000..774e7d4 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd @@ -0,0 +1,6 @@ +class_name GdUnitInit +extends GdUnitEvent + + +func _init() -> void: + _event_type = INIT diff --git a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid new file mode 100644 index 0000000..f2b572f --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid @@ -0,0 +1 @@ +uid://u23moapwuhhe diff --git a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd new file mode 100644 index 0000000..d7a3c11 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd @@ -0,0 +1,6 @@ +class_name GdUnitStop +extends GdUnitEvent + + +func _init() -> void: + _event_type = STOP diff --git a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid new file mode 100644 index 0000000..1ea6bf6 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid @@ -0,0 +1 @@ +uid://c6woj0e8wdqo7 diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd new file mode 100644 index 0000000..c6194ef --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd @@ -0,0 +1,19 @@ +class_name GdUnitEventTestDiscoverEnd +extends GdUnitEvent + + +var _total_testsuites: int + + +func _init(testsuite_count: int, test_count: int) -> void: + _event_type = DISCOVER_END + _total_testsuites = testsuite_count + _total_count = test_count + + +func total_test_suites() -> int: + return _total_testsuites + + +func total_tests() -> int: + return _total_count diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid new file mode 100644 index 0000000..9a4fdbd --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid @@ -0,0 +1 @@ +uid://ct6pal6upa625 diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd new file mode 100644 index 0000000..c7dd36f --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd @@ -0,0 +1,6 @@ +class_name GdUnitEventTestDiscoverStart +extends GdUnitEvent + + +func _init() -> void: + _event_type = DISCOVER_START diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid new file mode 100644 index 0000000..a7f99d9 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid @@ -0,0 +1 @@ +uid://vays2aajeqqi diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd new file mode 100644 index 0000000..52dab3f --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd @@ -0,0 +1,6 @@ +class_name GdUnitSessionClose +extends GdUnitEvent + + +func _init() -> void: + _event_type = SESSION_CLOSE diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid new file mode 100644 index 0000000..41b248d --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid @@ -0,0 +1 @@ +uid://dv2gtrby4spyt diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd new file mode 100644 index 0000000..420ad53 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd @@ -0,0 +1,6 @@ +class_name GdUnitSessionStart +extends GdUnitEvent + + +func _init() -> void: + _event_type = SESSION_START diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid new file mode 100644 index 0000000..ba9fbc1 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid @@ -0,0 +1 @@ +uid://cgj1lfm5eu0t8 diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd new file mode 100644 index 0000000..45960cc --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd @@ -0,0 +1,305 @@ +## The execution context +## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor +class_name GdUnitExecutionContext + +enum GC_ORPHANS_CHECK { + NONE, + SUITE_HOOK_AFTER, + TEST_HOOK_AFTER, + TEST_CASE +} + + +var _parent_context: GdUnitExecutionContext +var _sub_context: Array[GdUnitExecutionContext] = [] +var _orphan_monitor: GdUnitOrphanNodesMonitor +var _memory_observer: GdUnitMemoryObserver +var _report_collector: GdUnitTestReportCollector +var _timer: LocalTime +var _test_case_name: StringName +var _test_case_parameter_set: Array +var _name: String +var _test_execution_iteration: int = 0 +var _flaky_test_check := GdUnitSettings.is_test_flaky_check_enabled() +var _flaky_test_retries := GdUnitSettings.get_flaky_max_retries() +var _last_error: GdUnitError = null + + +var _settings_snapshot: GdUnitProjectSettingsSnapshot = null + + +var error_monitor: GodotGdErrorMonitor = null: + get: + if _parent_context != null: + return _parent_context.error_monitor + if error_monitor == null: + error_monitor = GodotGdErrorMonitor.new() + return error_monitor + + +var test_suite: GdUnitTestSuite = null: + get: + if _parent_context != null: + return _parent_context.test_suite + return test_suite + + +var test_case: _TestCase = null: + get: + if test_case == null and _parent_context != null: + return _parent_context.test_case + return test_case + + +func _init(name: StringName, parent_context: GdUnitExecutionContext = null) -> void: + _name = name + _parent_context = parent_context + _timer = LocalTime.now() + _orphan_monitor = GdUnitOrphanNodesMonitor.new(name) + + if parent_context != null: + parent_context._orphan_monitor.add_child_monitor(_orphan_monitor) + orphan_monitor_start() + + _memory_observer = GdUnitMemoryObserver.new() + _report_collector = GdUnitTestReportCollector.new() + if parent_context != null: + parent_context._sub_context.append(self) + + +func dispose() -> void: + if test_suite != null: + test_suite.free() + _timer = null + _orphan_monitor = null + _report_collector = null + _memory_observer = null + _parent_context = null + test_suite = null + test_case = null + dispose_sub_contexts() + + +func dispose_sub_contexts() -> void: + for context in _sub_context: + context.dispose() + _sub_context.clear() + + +func terminate() -> void: + if test_case: + test_case.do_terminate() + + +static func of(pe: GdUnitExecutionContext) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(pe._test_case_name, pe) + context._test_case_name = pe._test_case_name + context._test_execution_iteration = pe._test_execution_iteration + return context + + +static func of_test_case(pe: GdUnitExecutionContext, p_test_case: _TestCase) -> GdUnitExecutionContext: + assert(p_test_case, "test_case is null") + var context := GdUnitExecutionContext.new(p_test_case.test_name(), pe) + context.test_case = p_test_case + return context + + +static func of_parameterized_test(pe: GdUnitExecutionContext, test_case_name: String, test_case_parameter_set: Array) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(test_case_name, pe) + context._test_case_name = test_case_name + context._test_case_parameter_set = test_case_parameter_set + return context + + +func get_test_suite_path() -> String: + return test_suite.get_script().resource_path + + +func get_test_suite_name() -> StringName: + return test_suite.get_name() + + +func get_test_case_name() -> StringName: + if _test_case_name.is_empty(): + return test_case._test_case.display_name + return _test_case_name + + +func save_project_settings() -> void: + if _settings_snapshot == null: + _settings_snapshot = GdUnitProjectSettingsSnapshot.new() + _settings_snapshot.save() + + +func restore_project_settings() -> void: + if _settings_snapshot == null: + return + _settings_snapshot.restore() + + +func error_monitor_start() -> void: + error_monitor.start() + + +func error_monitor_stop() -> void: + error_monitor.stop() + for error_report in error_monitor.to_reports(): + if error_report.is_error(): + _report_collector.push_back(error_report) + + +func orphan_monitor_start() -> void: + _orphan_monitor.start() + + +func orphan_monitor_collect() -> void: + _orphan_monitor.collect() + + +func orphan_monitor_stop() -> void: + _orphan_monitor.stop() + + +func add_report(report: GdUnitReport) -> GdUnitReport: + _report_collector.push_back(report) + return report + + +func reports() -> Array[GdUnitReport]: + return _report_collector.reports() + + +func set_error(error: GdUnitError) -> void: + _last_error = error + + +func last_error() -> GdUnitError: + if _report_collector.reports().is_empty(): + return _last_error + + var last_report: GdUnitReport = _report_collector.reports()[-1] + return last_report._error if last_report != null else _last_error + + +func collect_reports(recursive: bool) -> Array[GdUnitReport]: + if not recursive: + return reports() + + # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` + # we strictly need to copy the reports before adding sub context reports to avoid manipulation of the current context + var current_reports := reports().duplicate() + for sub_context in _sub_context: + current_reports.append_array(sub_context.collect_reports(true)) + + return current_reports + + +func calculate_statistics(reports_: Array[GdUnitReport]) -> Dictionary: + var failed_count := GdUnitTestReportCollector.count_failures(reports_) + var error_count := GdUnitTestReportCollector.count_errors(reports_) + var warn_count := GdUnitTestReportCollector.count_warnings(reports_) + var skip_count := GdUnitTestReportCollector.count_skipped(reports_) + var orphan_count := GdUnitTestReportCollector.count_orphans(reports_) + var is_failed := !is_success() + var elapsed_time := _timer.elapsed_since_ms() + var retries := 1 if _parent_context == null else _sub_context.size() + # Mark as flaky if it is successful, but errors were counted + var is_flaky := retries > 1 and not is_failed + # In the case of a flakiness test, we do not report an error counter, as an unreliable test is considered successful + # after a certain number of repetitions. + if is_flaky: + failed_count = 0 + + return { + GdUnitEvent.RETRY_COUNT: retries, + GdUnitEvent.ELAPSED_TIME: elapsed_time, + GdUnitEvent.FAILED: is_failed, + GdUnitEvent.ERRORS: error_count > 0, + GdUnitEvent.WARNINGS: warn_count > 0, + GdUnitEvent.FLAKY: is_flaky, + GdUnitEvent.SKIPPED: skip_count > 0, + GdUnitEvent.FAILED_COUNT: failed_count, + GdUnitEvent.ERROR_COUNT: error_count, + GdUnitEvent.SKIPPED_COUNT: skip_count, + GdUnitEvent.ORPHAN_NODES: orphan_count, + } + + +func is_success() -> bool: + if _sub_context.is_empty(): + return not _report_collector.has_failures() + # we on test suite level? + if _parent_context == null: + return not _report_collector.has_failures() + + return _sub_context[-1].is_success() and not _report_collector.has_failures() + + +func is_skipped() -> bool: + return ( + _sub_context.any(func(c :GdUnitExecutionContext) -> bool: + return c.is_skipped()) + or test_case.is_skipped() if test_case != null else false + ) + + +func is_interupted() -> bool: + return false if test_case == null else test_case.is_interupted() + + +func sum(accum: int, number: int) -> int: + return accum + number + + +func retry_execution() -> bool: + var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries + if retry: + _test_execution_iteration += 1 + return retry + + +func register_auto_free(obj: Variant) -> Variant: + return _memory_observer.register_auto_free(obj) + + +## Runs the gdunit garbage collector to free registered object and handle orphan node reporting +func gc(gc_orphan_check: GC_ORPHANS_CHECK = GC_ORPHANS_CHECK.NONE) -> void: + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().clear_assert() + await _memory_observer.gc() + orphan_monitor_stop() + + match(gc_orphan_check): + GC_ORPHANS_CHECK.SUITE_HOOK_AFTER: + report_ophans(0, "\n\t[b]Verify your test suite setup![/b]") + + GC_ORPHANS_CHECK.TEST_HOOK_AFTER: + report_ophans(0, "\n\t[b]Verify before_test() and after_test()![/b]") + + GC_ORPHANS_CHECK.TEST_CASE: + report_ophans(test_case.line_number(),) + + +func report_ophans(line_number: int, additonal_info := "") -> void: + var orphan_infos := _orphan_monitor.detected_orphans() + if orphan_infos.is_empty(): + var orphans_count := _orphan_monitor.orphans_count() + if orphans_count > 0: + reports().append(GdUnitReport.new() \ + .create(GdUnitReport.ORPHAN, line_number, GdAssertMessages.orphan_warning(orphans_count) + additonal_info) + .with_current_value(orphans_count)) + else: + reports() \ + .append(GdUnitReport.new()\ + .create(GdUnitReport.ORPHAN, line_number, GdAssertMessages.orphan_detected(orphan_infos.size()) + additonal_info) \ + .with_current_value(orphan_infos.size())) + + for orphan_info in orphan_infos: + var error := GdUnitError.new( + GdAssertMessages.orphan_node_info(orphan_info), + 0, + GdUnitStackTrace.new([orphan_info._stack_element]) if orphan_info._stack_element != null else null) + reports().push_back(GdUnitReport.new()\ + .from_error(GdUnitReport.ORPHAN, error) + .with_current_value(0)) diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid new file mode 100644 index 0000000..68c3ce6 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid @@ -0,0 +1 @@ +uid://6ip54pfsumjv diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd new file mode 100644 index 0000000..dd03a31 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd @@ -0,0 +1,135 @@ +## The memory watcher for objects that have been registered and are released when 'gc' is called. +class_name GdUnitMemoryObserver +extends RefCounted + +const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_" +const TAG_AUTO_FREE = "GdUnit4_marked_auto_free" +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +var _store :Array[Variant] = [] +# enable for debugging purposes +var _is_stdout_verbose := false +const _show_debug := false + + +## Registration of an instance to be released when an execution phase is completed +func register_auto_free(obj :Variant) -> Variant: + if not is_instance_valid(obj): + return obj + # do not register on GDScriptNativeClass + @warning_ignore("unsafe_cast") + if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : + return obj + #if obj is GDScript or obj is ScriptExtension: + # return obj + if obj is MainLoop: + push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj) + return + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():register auto_free(%s)" % obj) + # only register pure objects + if obj is GdUnitSceneRunner: + _store.push_back(obj) + else: + _store.append(obj) + _tag_object(obj) + return obj + + +# to disable instance guard when run into issues. +static func _is_instance_guard_enabled() -> bool: + return false + + +static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: + if not _show_debug: + return + var script :GDScript= obj if obj is GDScript else obj.get_script() + if script: + var base_script :GDScript = script.get_base_script() + @warning_ignore("unsafe_method_access") + prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path) + if base_script: + debug_observe("+", base_script, indent+1) + else: + @warning_ignore("unsafe_method_access") + prints(name, obj, obj.get_class(), obj.get_name()) + + +static func guard_instance(obj :Object) -> void: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if Engine.has_meta(tag): + return + debug_observe("Gard on instance", obj) + Engine.set_meta(tag, obj) + + +static func unguard_instance(obj :Object, verbose := true) -> void: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if verbose: + debug_observe("unguard instance", obj) + if Engine.has_meta(tag): + Engine.remove_meta(tag) + + +static func gc_guarded_instance(name :String, instance :Object) -> void: + if not _is_instance_guard_enabled(): + return + await (Engine.get_main_loop() as SceneTree).process_frame + unguard_instance(instance, false) + if is_instance_valid(instance) and instance is RefCounted: + # finally do this very hacky stuff + # we need to manually unreferece to avoid leaked scripts + # but still leaked GDScriptFunctionState exists + #var script :GDScript = instance.get_script() + #if script: + # var base_script :GDScript = script.get_base_script() + # if base_script: + # base_script.unreference() + debug_observe(name, instance) + (instance as RefCounted).unreference() + await (Engine.get_main_loop() as SceneTree).process_frame + + +static func gc_on_guarded_instances() -> void: + if not _is_instance_guard_enabled(): + return + for tag in Engine.get_meta_list(): + if tag.begins_with(TAG_OBSERVE_INSTANCE): + var instance :Object = Engine.get_meta(tag) + await gc_guarded_instance("Leaked instance detected:", instance) + await GdUnitTools.free_instance(instance, false) + + +# store the object into global store aswell to be verified by 'is_marked_auto_free' +func _tag_object(obj :Variant) -> void: + var tagged_object: Array = Engine.get_meta(TAG_AUTO_FREE, []) + tagged_object.append(obj) + Engine.set_meta(TAG_AUTO_FREE, tagged_object) + + +## Runs over all registered objects and releases them +func gc() -> void: + if _store.is_empty(): + return + # give engine time to free objects to process objects marked by queue_free() + await (Engine.get_main_loop() as SceneTree).process_frame + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size()) + var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, []) + while not _store.is_empty(): + var value :Variant = _store.pop_front() + tagged_objects.erase(value) + await GdUnitTools.free_instance(value, _is_stdout_verbose) + assert(_store.is_empty(), "The memory observer has still entries in the store!") + + +## Checks whether the specified object is registered for automatic release +static func is_marked_auto_free(obj: Variant) -> bool: + var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, []) + return tagged_objects.has(obj) diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid new file mode 100644 index 0000000..75283a3 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid @@ -0,0 +1 @@ +uid://btukdpj6qv4qt diff --git a/addons/gdUnit4/src/core/execution/GdUnitProjectSettingsSnapshot.gd b/addons/gdUnit4/src/core/execution/GdUnitProjectSettingsSnapshot.gd new file mode 100644 index 0000000..af7336d --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitProjectSettingsSnapshot.gd @@ -0,0 +1,36 @@ +## Captures and restores all in-memory [ProjectSettings] values around a single +## stage of test execution.[br] +## Each [GdUnitExecutionContext] owns one instance. Suite-level and test-level +## isolation are achieved by the two separate contexts rather than by stacking:[br] +## [codeblock] +## GdUnitTestSuiteExecutionStage -> save() (before before()) +## GdUnitTestCaseExecutionStage -> save() (before before_test()) +## GdUnitTestCaseExecutionStage -> restore() (after after_test()) +## GdUnitTestSuiteExecutionStage -> restore() (after after()) +## [/codeblock] +class_name GdUnitProjectSettingsSnapshot +extends RefCounted + + +var _snapshot: Dictionary = {} + + +func save() -> void: + _snapshot.clear() + for property: Dictionary in ProjectSettings.get_property_list(): + var name: String = property["name"] + if ProjectSettings.has_setting(name): + var value: Variant = ProjectSettings.get_setting(name) + @warning_ignore("unsafe_method_access") + _snapshot[name] = value.duplicate() if (value is Dictionary or value is Array) else value + + +func restore() -> void: + for name: String in _snapshot: + if not ProjectSettings.has_setting(name): + continue + var current: Variant = ProjectSettings.get_setting(name) + var original: Variant = _snapshot[name] + if current != original: + ProjectSettings.set_setting(name, original) + _snapshot.clear() diff --git a/addons/gdUnit4/src/core/execution/GdUnitProjectSettingsSnapshot.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitProjectSettingsSnapshot.gd.uid new file mode 100644 index 0000000..464df39 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitProjectSettingsSnapshot.gd.uid @@ -0,0 +1 @@ +uid://d1p18amsqefsy diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd new file mode 100644 index 0000000..91247bd --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd @@ -0,0 +1,77 @@ +# Collects all reports seperated as warnings, failures and errors +class_name GdUnitTestReportCollector +extends RefCounted + + +var _reports :Array[GdUnitReport] = [] + + +static func __filter_is_error(report :GdUnitReport) -> bool: + return report.is_error() + + +static func __filter_is_failure(report :GdUnitReport) -> bool: + return report.is_failure() + + +static func __filter_is_warning(report :GdUnitReport) -> bool: + return report.is_warning() + + +static func __filter_is_skipped(report :GdUnitReport) -> bool: + return report.is_skipped() + + +static func __filter_is_orphan(report: GdUnitReport) -> bool: + return report.is_orphan() + + +static func count_failures(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_failure).size() + + +static func count_errors(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_error).size() + + +static func count_warnings(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_warning).size() + + +static func count_skipped(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_skipped).size() + + +static func count_orphans(reports_: Array[GdUnitReport]) -> int: + var orphan_reports := reports_.filter(__filter_is_orphan) + if orphan_reports.is_empty(): + return 0 + ## Collect orphan count from the reports + var orphans := 0 + for report: GdUnitReport in orphan_reports: + orphans += report._current_value + return orphans + + +func has_failures() -> bool: + return _reports.any(__filter_is_failure) + + +func has_errors() -> bool: + return _reports.any(__filter_is_error) + + +func has_warnings() -> bool: + return _reports.any(__filter_is_warning) + + +func has_skipped() -> bool: + return _reports.any(__filter_is_skipped) + + +func reports() -> Array[GdUnitReport]: + return _reports + + +func push_back(report :GdUnitReport) -> void: + _reports.push_back(report) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid new file mode 100644 index 0000000..0f6bfe9 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid @@ -0,0 +1 @@ +uid://ctxroa1o5g3gr diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd new file mode 100644 index 0000000..dfb4441 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd @@ -0,0 +1,56 @@ +## The executor to run a test-suite +class_name GdUnitTestSuiteExecutor + + +# preload all asserts here +@warning_ignore("unused_private_class_variable") +var _assertions := GdUnitAssertions.new() +var _executeStage := GdUnitTestSuiteExecutionStage.new() +var _debug_mode : bool +var _terminated := false + +func _init(debug_mode: bool = false) -> void: + _executeStage.set_debug_mode(debug_mode) + _debug_mode = debug_mode + GdUnitSignals.instance().gdunit_test_session_terminate.connect(_on_testsession_terminated) + + +func _on_testsession_terminated() -> void: + _terminated = true + GdUnitThreadManager.interrupt() + + +func run_and_wait(tests: Array[GdUnitTestCase]) -> void: + if !_debug_mode: + GdUnitSignals.instance().gdunit_event.emit(GdUnitInit.new()) + + var orphan_detection_enabled := GdUnitSettings.is_verbose_orphans() + if not orphan_detection_enabled: + prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.") + + # first we group all tests by resource path + var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.suite_resource_path + ) + var scanner := GdUnitTestSuiteScanner.new() + for suite_path: String in grouped_by_suites.keys(): + if _terminated: + break + @warning_ignore("unsafe_call_argument") + var suite_tests: Array[GdUnitTestCase] = Array(grouped_by_suites[suite_path], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(suite_path) + if script.get_class() == "GDScript": + var context := GdUnitExecutionContext.new(suite_path) + var test_suite := scanner.load_suite(script as GDScript, suite_tests) + context.test_suite = test_suite + (Engine.get_main_loop() as SceneTree).root.add_child(test_suite) + await _executeStage.execute(context) + context.dispose() + else: + await GdUnit4CSharpApiLoader.execute(suite_tests) + if !_debug_mode: + GdUnitSignals.instance().gdunit_event.emit(GdUnitStop.new()) + + +func fail_fast(enabled :bool) -> void: + _executeStage.fail_fast(enabled) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid new file mode 100644 index 0000000..c62fc8f --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid @@ -0,0 +1 @@ +uid://bc4g8p4qcusnu diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd new file mode 100644 index 0000000..f106f21 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd @@ -0,0 +1,22 @@ +## The test case shutdown hook implementation.[br] +## It executes the 'test_after()' block from the test-suite. +class_name GdUnitTestCaseAfterStage +extends IGdUnitExecutionStage + + +var _call_stage: bool + + +func _init(call_stage := true) -> void: + _call_stage = call_stage + + +func _execute(context: GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.after_test() + + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) + context.error_monitor_stop() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid new file mode 100644 index 0000000..1e3de57 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid @@ -0,0 +1 @@ +uid://bbhrrm627323b diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd new file mode 100644 index 0000000..d7790a9 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd @@ -0,0 +1,20 @@ +## The test case startup hook implementation.[br] +## It executes the 'test_before()' block from the test-suite. +class_name GdUnitTestCaseBeforeStage +extends IGdUnitExecutionStage + +var _call_stage :bool + + +func _init(call_stage := true) -> void: + _call_stage = call_stage + + +func _execute(context: GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.before_test() + + context.error_monitor_start() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid new file mode 100644 index 0000000..013b3fa --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid @@ -0,0 +1 @@ +uid://bd1uwwkatl021 diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd new file mode 100644 index 0000000..9545058 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd @@ -0,0 +1,39 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseExecutionStage +extends IGdUnitExecutionStage + + +var _stage_single_test: IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new() +var _stage_fuzzer_test: IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new() + + +## Executes the test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_before() [br] +## -> test_case() [br] +## -> test_after() [br] +@warning_ignore("redundant_await") +func _execute(context :GdUnitExecutionContext) -> void: + var test_case := context.test_case + + context.save_project_settings() + context.error_monitor_start() + + if test_case.is_fuzzed(): + await _stage_fuzzer_test.execute(context) + else: + await _stage_single_test.execute(context) + + await context.gc() + context.error_monitor_stop() + context.restore_project_settings() + + # finally free the test instance + if is_instance_valid(context.test_case): + context.test_case.dispose() + + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_single_test.set_debug_mode(debug_mode) + _stage_fuzzer_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid new file mode 100644 index 0000000..b332ad9 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://dvq7viruup8w7 diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd new file mode 100644 index 0000000..f3971ab --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd @@ -0,0 +1,32 @@ +## The test suite shutdown hook implementation.[br] +## It executes the 'after()' block from the test-suite. +class_name GdUnitTestSuiteAfterStage +extends IGdUnitExecutionStage + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + @warning_ignore("redundant_await") + await test_suite.after() + # TODO Godot 4.7.beta: This is current an workaround to fix sig11 crash on orphan tests + # is releated to https://github.com/godot-gdunit-labs/gdUnit4/issues/1154 + await (Engine.get_main_loop() as SceneTree).process_frame + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + + var reports := context.collect_reports(false) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new()\ + .suite_after(context.get_test_suite_path(),\ + test_suite.get_name(), + statistics, + reports)) + GdUnitFileAccess.clear_tmp() + # Guard that checks if all doubled (spy/mock) objects are released + await GdUnitClassDoubler.check_leaked_instances() + # we hide the scene/main window after runner is finished + if not Engine.is_embedded_in_editor(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid new file mode 100644 index 0000000..cce2b43 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid @@ -0,0 +1 @@ +uid://dydlanjslgwae diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd new file mode 100644 index 0000000..a6ed8c5 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd @@ -0,0 +1,14 @@ +## The test suite startup hook implementation.[br] +## It executes the 'before()' block from the test-suite. +class_name GdUnitTestSuiteBeforeStage +extends IGdUnitExecutionStage + + +func _execute(context: GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + fire_event(GdUnitEvent.new()\ + .suite_before(context.get_test_suite_path(), test_suite.get_name(), test_suite.get_child_count())) + + @warning_ignore("redundant_await") + await test_suite.before() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid new file mode 100644 index 0000000..eb7826d --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid @@ -0,0 +1 @@ +uid://c73556hcyufi0 diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd new file mode 100644 index 0000000..cd81c2e --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd @@ -0,0 +1,149 @@ +## The test suite main execution stage.[br] +class_name GdUnitTestSuiteExecutionStage +extends IGdUnitExecutionStage + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _stage_before :IGdUnitExecutionStage = GdUnitTestSuiteBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestSuiteAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseExecutionStage.new() +var _fail_fast := false + + +## Executes all tests of an test suite.[br] +## It executes synchronized following stages[br] +## -> before() [br] +## -> run all test cases [br] +## -> after() [br] +func _execute(context :GdUnitExecutionContext) -> void: + if context.test_suite.__is_skipped: + await fire_test_suite_skipped(context) + else: + @warning_ignore("return_value_discarded") + GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter) + context.save_project_settings() + await _stage_before.execute(context) + for test_case_index in context.test_suite.get_child_count(): + # iterate only over test cases + var test_case := context.test_suite.get_child(test_case_index) as _TestCase + if not is_instance_valid(test_case): + continue + context.test_suite.set_active_test_case(test_case.test_name()) + var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case) + await _stage_test.execute(test_case_context) + # stop on first error or if fail fast is enabled + if test_case.is_terminated() or (_fail_fast and not test_case_context.is_success()): + break + if test_case.is_interupted(): + # it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out + # we delete the current test suite where is execute the current test case to kill the function state + # and replace it by a clone without function state + context.test_suite = await clone_test_suite(context.test_suite) + await _stage_after.execute(context) + context.restore_project_settings() + GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter) + + await (Engine.get_main_loop() as SceneTree).process_frame + + +# clones a test suite and moves the test cases to new instance +func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: + await (Engine.get_main_loop() as SceneTree).process_frame + dispose_timers(test_suite) + await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter) + var parent := test_suite.get_parent() + var _test_suite := GdUnitTestSuite.new() + parent.remove_child(test_suite) + copy_properties(test_suite, _test_suite) + for child in test_suite.get_children(): + test_suite.remove_child(child) + _test_suite.add_child(child) + parent.add_child(_test_suite) + @warning_ignore("return_value_discarded") + GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter) + # finally free current test suite instance + test_suite.free() + await (Engine.get_main_loop() as SceneTree).process_frame + return _test_suite + + +func dispose_timers(test_suite :GdUnitTestSuite) -> void: + GdUnitTools.release_timers() + for child in test_suite.get_children(): + if child is Timer: + (child as Timer).stop() + test_suite.remove_child(child) + child.free() + + +func copy_properties(source :Object, target :Object) -> void: + if not source is _TestCase and not source is GdUnitTestSuite: + return + for property in source.get_property_list(): + var property_name :String = property["name"] + if property_name == "__awaiter": + continue + target.set(property_name, source.get(property_name)) + + +func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var skip_count := test_suite.get_child_count() + fire_event(GdUnitEvent.new()\ + .suite_before(context.get_test_suite_path(), test_suite.get_name(), skip_count)) + + + for test_case_index in context.test_suite.get_child_count(): + # iterate only over test cases + var test_case := context.test_suite.get_child(test_case_index) as _TestCase + if not is_instance_valid(test_case): + continue + var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case) + fire_event(GdUnitEvent.new().test_before(test_case.id())) + # use skip count 0 because we counted it over the complete test suite + fire_test_skipped(test_case_context, 0) + + + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: skip_count, + GdUnitEvent.SKIPPED: true + } + var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count)) + fire_event(GdUnitEvent.new().suite_after(context.get_test_suite_path(), test_suite.get_name(), statistics, [report])) + await (Engine.get_main_loop() as SceneTree).process_frame + + +func fire_test_skipped(context: GdUnitExecutionContext, skip_count := 1) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: skip_count, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped("Skipped from the entire test suite")) + fire_event(GdUnitEvent.new().test_after(test_case.id(), test_case.test_name(), statistics, [report])) + + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fail_fast(enabled :bool) -> void: + _fail_fast = enabled diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid new file mode 100644 index 0000000..b78cabc --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://dcpwobxwubuyg diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd new file mode 100644 index 0000000..a04f7e7 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd @@ -0,0 +1,37 @@ +## The interface of execution stage.[br] +## An execution stage is defined as an encapsulated task that can execute 1-n substages covered by its own execution context.[br] +## Execution stage are always called synchronously. +@abstract class_name IGdUnitExecutionStage +extends RefCounted + +var _debug_mode := false + + +## Executes synchronized the implemented stage in its own execution context.[br] +## example:[br] +## [codeblock] +## # waits for 100ms +## await MyExecutionStage.new().execute() +## [/codeblock][br] +func execute(context: GdUnitExecutionContext) -> void: + GdUnitThreadManager.get_current_context().set_execution_context(context) + @warning_ignore("redundant_await") + await _execute(context) + + +## Sends the event to registered listeners +func fire_event(event: GdUnitEvent) -> void: + if _debug_mode: + GdUnitSignals.instance().gdunit_event_debug.emit(event) + else: + GdUnitSignals.instance().gdunit_event.emit(event) + + +## Internal testing stuff.[br] +## Sets the executor into debug mode to emit `GdUnitEvent` via signal `gdunit_event_debug` +func set_debug_mode(debug_mode: bool) -> void: + _debug_mode = debug_mode + + +## The execution phase to be carried out. +@abstract func _execute(context: GdUnitExecutionContext) -> void diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid new file mode 100644 index 0000000..ddd89a1 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://05sjvx0vw2m6 diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd new file mode 100644 index 0000000..c1e0924 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd @@ -0,0 +1,52 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseFuzzedExecutionStage +extends IGdUnitExecutionStage + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + fire_event(GdUnitEvent.new().test_before(context.test_case.id())) + + while context.retry_execution(): + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(test_context)) + await _stage_after.execute(test_context) + if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): + break + + context.gc() + if context.is_skipped(): + fire_test_skipped(context) + else: + var reports: = context.collect_reports(true) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new().test_after(context.test_case.id(), context.test_case.test_name(), statistics, reports)) + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fire_test_skipped(context: GdUnitExecutionContext) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new().test_after(test_case.id(), test_case.test_name(), statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid new file mode 100644 index 0000000..33491c6 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://e0ldemh653g3 diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd new file mode 100644 index 0000000..a556758 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd @@ -0,0 +1,56 @@ +## The fuzzed test case execution stage.[br] +class_name GdUnitTestCaseFuzzedTestStage +extends IGdUnitExecutionStage + +var _expression_runner := GdUnitExpressionRunner.new() + + +## Executes a test case with given fuzzers 'test_()' iterative.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_case := context.test_case + var fuzzers := create_fuzzers(test_suite, test_case) + + # guard on fuzzers + for fuzzer in fuzzers: + @warning_ignore("return_value_discarded") + GdUnitMemoryObserver.guard_instance(fuzzer) + + for iteration in test_case.iterations(): + @warning_ignore("redundant_await") + await test_suite.before_test() + await test_case.execute(fuzzers, iteration) + @warning_ignore("redundant_await") + await test_suite.after_test() + if test_case.is_interupted(): + break + # interrupt at first failure + var reports := context.reports() + if not reports.is_empty(): + var report :GdUnitReport = reports.pop_front() + reports.append(GdUnitReport.new() \ + .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) + break + + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) + + # unguard on fuzzers + if not test_case.is_interupted(): + for fuzzer in fuzzers: + GdUnitMemoryObserver.unguard_instance(fuzzer) + + +func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]: + if not test_case.is_fuzzed(): + return Array() + test_case.generate_seed() + var fuzzers :Array[Fuzzer] = [] + for fuzzer_arg in test_case.fuzzer_arguments(): + @warning_ignore("unsafe_cast") + var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script() as GDScript, fuzzer_arg.plain_value() as String) + fuzzer._iteration_index = 0 + fuzzer._iteration_limit = test_case.iterations() + fuzzers.append(fuzzer) + return fuzzers diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid new file mode 100644 index 0000000..1da4d36 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid @@ -0,0 +1 @@ +uid://cjrmptimnpouh diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd new file mode 100644 index 0000000..03f8fa5 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd @@ -0,0 +1,53 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseSingleExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + fire_event(GdUnitEvent.new().test_before(context.test_case.id())) + while context.retry_execution(): + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + if not test_context.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(test_context)) + await _stage_after.execute(test_context) + if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): + break + + context.gc() + if context.is_skipped(): + fire_test_skipped(context) + else: + var reports: = context.collect_reports(true) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new().test_after(context.test_case.id(), context.test_case.test_name(), statistics, reports)) + + +func set_debug_mode(debug_mode :bool = false) -> void: + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fire_test_skipped(context: GdUnitExecutionContext) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new().test_after(test_case.id(), test_case.test_name(), statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid new file mode 100644 index 0000000..13ce554 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid @@ -0,0 +1 @@ +uid://bnobmml1emlf8 diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd new file mode 100644 index 0000000..b8ace50 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd @@ -0,0 +1,11 @@ +## The single test case execution stage.[br] +class_name GdUnitTestCaseSingleTestStage +extends IGdUnitExecutionStage + + +## Executes a single test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context: GdUnitExecutionContext) -> void: + await context.test_case.execute() + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid new file mode 100644 index 0000000..0dc5d49 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid @@ -0,0 +1 @@ +uid://b5wo04i1echop diff --git a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd new file mode 100644 index 0000000..0f87ad1 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd @@ -0,0 +1,78 @@ +class_name GdUnitBaseReporterTestSessionHook +extends GdUnitTestSessionHook + + +var test_session: GdUnitTestSession: + get: + return test_session + set(value): + # disconnect first possible connected listener + if test_session != null: + test_session.test_event.disconnect(_on_test_event) + # add listening to current session + test_session = value + if test_session != null: + test_session.test_event.connect(_on_test_event) + + +var _report_summary: GdUnitReportSummary +var _reporter: GdUnitTestReporter +var _report_writer: GdUnitReportWriter +var _report_converter: Callable + +func _init(report_writer: GdUnitReportWriter, hook_name: String, hook_description: String, report_converter: Callable) -> void: + super(hook_name, hook_description) + _reporter = GdUnitTestReporter.new() + _report_writer = report_writer + _report_converter = report_converter + + +func startup(session: GdUnitTestSession) -> GdUnitResult: + test_session = session + _report_summary = GdUnitReportSummary.new(_report_converter) + _reporter.init_summary() + + return GdUnitResult.success() + + +func shutdown(session: GdUnitTestSession) -> GdUnitResult: + var report_path := _report_writer.write(session.report_path, _report_summary) + session.send_message("Open {0} Report at: file://{1}".format([_report_writer.output_format(), report_path])) + + return GdUnitResult.success() + + +func _on_test_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.TESTSUITE_BEFORE: + _reporter.init_statistics() + _report_summary.add_testsuite_report(event.resource_path(), event.suite_name(), event.total_count()) + GdUnitEvent.TESTSUITE_AFTER: + var statistics := _reporter.build_test_suite_statisitcs(event) + _report_summary.update_testsuite_counters( + event.resource_path(), + _reporter.error_count(statistics), + _reporter.failed_count(statistics), + _reporter.orphan_nodes(statistics), + _reporter.skipped_count(statistics), + _reporter.flaky_count(statistics), + event.elapsed_time()) + _report_summary.add_testsuite_reports( + event.resource_path(), + event.reports() + ) + GdUnitEvent.TESTCASE_BEFORE: + var test := test_session.find_test_by_id(event.guid()) + _report_summary.add_testcase(test.source_file, test.suite_name, test.display_name) + GdUnitEvent.TESTCASE_AFTER: + _reporter.add_test_statistics(event) + var test := test_session.find_test_by_id(event.guid()) + _report_summary.set_counters(test.source_file, + test.display_name, + event.error_count(), + event.failed_count(), + event.orphan_nodes(), + event.is_skipped(), + event.is_flaky(), + event.elapsed_time()) + _report_summary.add_reports(test.source_file, test.display_name, event.reports()) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid new file mode 100644 index 0000000..6878a0f --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://cahjl1tte1fum diff --git a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd new file mode 100644 index 0000000..4b8f390 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd @@ -0,0 +1,9 @@ +class_name GdUnitHtmlReporterTestSessionHook +extends GdUnitBaseReporterTestSessionHook + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _init() -> void: + super(GdUnitHtmlReportWriter.new(), "GdUnitHtmlTestReporter", "The Html test reporting hook.", GdUnitTools.richtext_normalize) + set_meta("SYSTEM_HOOK", true) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid new file mode 100644 index 0000000..7edb491 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://drjauea5krxj2 diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd new file mode 100644 index 0000000..23850e4 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd @@ -0,0 +1,111 @@ +## @since GdUnit4 5.1.0 +## +## Base class for creating custom test session hooks in GdUnit4.[br] +## [br] +## [i]Test session hooks allow users to extend the GdUnit4 test framework by providing +## custom functionality that runs at specific points during the test execution lifecycle. +## This base class defines the interface that all test session hooks must implement.[/i] +## [br] +## [br] +## [b][u]Usage[/u][/b][br] +## 1. Create a new class that extends GdUnitTestSessionHook[br] +## 2. Override the required methods (startup, shutdown)[br] +## 3. Register your hook with the test engine (using the GdUnit4 settings dialog)[br] +## [br] +## [b][u]Example[/u][/b] +## [codeblock] +## class_name MyCustomTestHook +## extends GdUnitTestSessionHook +## +## func _init(): +## super("MyHook", "This is a description") +## +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Custom hook initialized") +## # Initialize resources, setup test environment, etc. +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Custom hook cleanup completed") +## # Cleanup resources, generate reports, etc. +## return GdUnitResult.success() +## [/codeblock] +## +## [b][u]Hook Lifecycle[/u][/b][br] +## 1. [i][b]Registration[/b][/i]: Hooks are registered with the test engine via settings dialog[br] +## 2. [i][b]Priority Sorting[/b][/i]: Hooks are sorted by priority[br] +## 3. [i][b]Startup[/b][/i]: startup() is called before test execution begins, if it returns an error is shown in the console[br] +## 4. [i][b]Test Execution[/b][/i]: Tests run normally (only if all hooks started successfully)[br] +## 5. [i][b]Shutdown[/b][/i]: shutdown() is called after all tests complete, regardless of startup success[br] +## [br] +## [b][u]Priority System[/u][/b][br] +## The priority system allows controlling the execution order of multiple hooks.[br] +## - The order can be changed in the GdUnit4 settings dialog.[br] +## - The priority of system hooks cannot be changed and they cannot be deleted.[br] +## [br] +## [b][u]Session Access[/u][/b][br] +## +## Both [i]startup()[/i] and [i]shutdown()[/i] methods receive a [GdUnitTestSession] parameter that provides:[br] +## - Access to test cases being executed[br] +## - Event emission capabilities for test progress tracking[br] +## - Message sending functionality for logging and communication[br] +class_name GdUnitTestSessionHook +extends RefCounted + + +## The display name of this hook. +var name: String: + get: + return name + + +## A detailed description of what this hook does. +var description: String: + get: + return description + + +## Initializes a new test session hook. +## +## [param _name] The display name for this hook +## [param _description] A detailed description of the hook's functionality +func _init(_name: String, _description: String) -> void: + self.name = _name + self.description = _description + + +## Called when the test session starts up, before any tests are executed.[br] +## [br] +## [color=yellow][i]This method should be overridden to implement custom initialization logic[/i][/color][br] +## [br] +## such as:[br] +## - Setting up test databases or external services[br] +## - Initializing mock objects or test fixtures[br] +## - Configuring logging or reporting systems[br] +## - Preparing the test environment[br] +## - Subscribing to test events via the session[br] +## [br] +## [param session] The test session instance providing access to test data and communication[br] +## [b]return:[/b] [code]GdUnitResult.success()[/code] if initialization succeeds, or [code]GdUnitResult.error("error")[/code] with +## an error message if initialization fails. +func startup(_session: GdUnitTestSession) -> GdUnitResult: + return GdUnitResult.error("%s:startup is not implemented" % get_script().resource_path) + + +## Called when the test session shuts down, after all tests have completed.[br] +## [br] +## [color=yellow][i]This method should be overridden to implement custom cleanup logic[/i][/color][br] +## [br] +## such as:[br] +## - Cleaning up test databases or external services[br] +## - Generating test reports or artifacts[br] +## - Releasing resources allocated during startup[br] +## - Performing final validation or assertions[br] +## - Processing collected test events and data[br] +## [br] +## [param session] The test session instance providing access to test results and communication[br] +## [b]return:[/b] [code]GdUnitResult.success()[/code] if cleanup succeeds, or [code]GdUnitResult.error("error")[/code] with +## an error message if cleanup fails. Cleanup errors are typically logged +## but don't prevent the test engine from shutting down. +func shutdown(_session: GdUnitTestSession) -> GdUnitResult: + return GdUnitResult.error("%s:shutdown is not implemented" % get_script().resource_path) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid new file mode 100644 index 0000000..e127ac6 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://kpv60yfrr4ha diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd new file mode 100644 index 0000000..d2e7b80 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd @@ -0,0 +1,192 @@ +class_name GdUnitTestSessionHookService +extends Object + + +var enigne_hooks: Array[GdUnitTestSessionHook] = []: + get: + return enigne_hooks + set(value): + enigne_hooks.append(value) + + +var _save_settings: bool = false + + +static func instance() -> GdUnitTestSessionHookService: + return GdUnitSingleton.instance("GdUnitTestSessionHookService", func()->GdUnitTestSessionHookService: + GdUnitSignals.instance().gdunit_message.emit("Installing GdUnit4 session system hooks.") + var service := GdUnitTestSessionHookService.new() + # Register default system hooks here + service._save_settings = false + service.register(GdUnitHtmlReporterTestSessionHook.new()) + service.register(GdUnitXMLReporterTestSessionHook.new()) + service.load_hook_settings() + service._save_settings = true + return service + ) + + +static func contains_hook(current: GdUnitTestSessionHook, other: GdUnitTestSessionHook) -> bool: + return current.get_script().resource_path == other.get_script().resource_path + + +func find_custom(hook: GdUnitTestSessionHook) -> int: + for index in enigne_hooks.size(): + if contains_hook.call(enigne_hooks[index], hook): + return index + return -1 + + +func load_hook(hook_resourc_path: String) -> GdUnitResult: + if !FileAccess.file_exists(hook_resourc_path): + return GdUnitResult.error("The hook '%s' not exists." % hook_resourc_path) + var script: GDScript = load(hook_resourc_path) + if script.get_base_script() != GdUnitTestSessionHook: + return GdUnitResult.error("The hook '%s' must inhertit from 'GdUnitTestSessionHook'." % hook_resourc_path) + + return GdUnitResult.success(script.new()) + + +func enable_hook(hook: GdUnitTestSessionHook, enabled: bool) -> void: + _enable_hook(hook, enabled) + GdUnitSignals.instance().gdunit_message.emit("Session hook '{name}' {enabled}.".format({ + "name": hook.name, + "enabled": "enabled" if enabled else "disabled"}) + ) + save_hock_setttings() + + +func register(hook: GdUnitTestSessionHook, enabled: bool = true) -> GdUnitResult: + if find_custom(hook) != -1: + return GdUnitResult.error("A hook instance of '%s' is already registered." % hook.get_script().resource_path) + + _enable_hook(hook, enabled) + enigne_hooks.append(hook) + save_hock_setttings() + GdUnitSignals.instance().gdunit_message.emit("Session hook '%s' installed." % hook.name) + + return GdUnitResult.success() + + +func unregister(hook: GdUnitTestSessionHook) -> GdUnitResult: + var hook_index := find_custom(hook) + if hook_index == -1: + return GdUnitResult.error("The hook instance of '%s' is NOT registered." % hook.get_script().resource_path) + + enigne_hooks.remove_at(hook_index) + save_hock_setttings() + return GdUnitResult.success() + + +func move_before(hook: GdUnitTestSessionHook, before: GdUnitTestSessionHook) -> void: + var before_index := find_custom(before) + var hook_index := find_custom(hook) + + # Verify the hook to move is behind the hook to be moved + if before_index >= hook_index: + return + + enigne_hooks.remove_at(hook_index) + enigne_hooks.insert(before_index, hook) + save_hock_setttings() + + +func move_after(hook: GdUnitTestSessionHook, after: GdUnitTestSessionHook) -> void: + var after_index := find_custom(after) + var hook_index := find_custom(hook) + + # Verify the hook to move is before the hook to be moved + if after_index <= hook_index: + return + + enigne_hooks.remove_at(hook_index) + enigne_hooks.insert(after_index, hook) + save_hock_setttings() + + +func execute_startup(session: GdUnitTestSession) -> GdUnitResult: + return await execute("startup", session) + + +func execute_shutdown(session: GdUnitTestSession) -> GdUnitResult: + return await execute("shutdown", session, true) + + +func execute(hook_func: String, session: GdUnitTestSession, reverse := false) -> GdUnitResult: + var failed_hook_calls: Array[GdUnitResult] = [] + + for hook_index in enigne_hooks.size(): + var index := enigne_hooks.size()-hook_index-1 if reverse else hook_index + var hook: = enigne_hooks[index] + if not is_enabled(hook): + continue + if OS.is_stdout_verbose(): + GdUnitSignals.instance().gdunit_message.emit("Session hook '%s' > %s()" % [hook.name, hook_func]) + var result: GdUnitResult = await hook.call(hook_func, session) + if result == null: + failed_hook_calls.push_back(GdUnitResult.error("Result is null! Check '%s'" % hook.get_script().resource_path)) + elif result.is_error(): + failed_hook_calls.push_back(result) + + if failed_hook_calls.is_empty(): + return GdUnitResult.success() + + var errors := failed_hook_calls.map(func(result: GdUnitResult) -> String: + return "Hook call '%s' failed with error: '%s'" % [hook_func, result.error_message()] + ) + return GdUnitResult.error( "\n".join(errors)) + + +func save_hock_setttings() -> void: + if not _save_settings: + return + + var hooks_to_save: Dictionary[String, bool] = {} + for hook in enigne_hooks: + var enabled: bool = hook.get_meta("enabled") + hooks_to_save[hook.get_script().resource_path] = enabled + + GdUnitSettings.set_session_hooks(hooks_to_save) + + +func load_hook_settings() -> void: + var hooks_resource_paths := GdUnitSettings.get_session_hooks() + if hooks_resource_paths.is_empty(): + return + + for hock_path: String in hooks_resource_paths.keys(): + var enabled := hooks_resource_paths[hock_path] + + # Do not reinstall already installed hooks + var existing_hooks := enigne_hooks.filter(func(element: GdUnitTestSessionHook) -> bool: + return element.get_script().resource_path == hock_path + ) + var existing_hook: GdUnitTestSessionHook = null if existing_hooks.is_empty() else existing_hooks.front() + # Applay enabled settings + if existing_hook != null: + _enable_hook(existing_hook, enabled) + continue + + # Load additional hooks + var result := load_hook(hock_path) + if result.is_error(): + push_error(result.error_message()) + continue + + GdUnitSignals.instance().gdunit_message.emit("Installing GdUnit4 session hooks.") + var hook: GdUnitTestSessionHook = result.value() + + result = register(hook, enabled) + if result.is_error(): + push_error(result.error_message()) + continue + + +static func is_enabled(hook: GdUnitTestSessionHook) -> bool: + if hook.has_meta("enabled"): + return hook.get_meta("enabled") + return true + + +func _enable_hook(hook: GdUnitTestSessionHook, enabled: bool) -> void: + hook.set_meta("enabled", enabled) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid new file mode 100644 index 0000000..97d07c7 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid @@ -0,0 +1 @@ +uid://dsfytfwbc035i diff --git a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd new file mode 100644 index 0000000..94caef5 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd @@ -0,0 +1,11 @@ +class_name GdUnitXMLReporterTestSessionHook +extends GdUnitBaseReporterTestSessionHook + + +func _init() -> void: + super(JUnitXmlReportWriter.new(), "GdUnitXMLTestReporter", "The JUnit XML test reporting hook.", convert_report_message) + set_meta("SYSTEM_HOOK", true) + + +func convert_report_message(value: String) -> String: + return value diff --git a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid new file mode 100644 index 0000000..68ab39d --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://jf37ss72amgb diff --git a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd new file mode 100644 index 0000000..fc83742 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd @@ -0,0 +1,25 @@ +class_name GdClassDescriptor +extends RefCounted + + +var _name :String +var _is_inner_class :bool +var _functions :Array[GdFunctionDescriptor] + + +func _init(p_name :String, p_is_inner_class :bool, p_functions :Array[GdFunctionDescriptor]) -> void: + _name = p_name + _is_inner_class = p_is_inner_class + _functions = p_functions + + +func name() -> String: + return _name + + +func is_inner_class() -> bool: + return _is_inner_class + + +func functions() -> Array[GdFunctionDescriptor]: + return _functions diff --git a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid new file mode 100644 index 0000000..4f76a89 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid @@ -0,0 +1 @@ +uid://dk1f7e7hgs5io diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd new file mode 100644 index 0000000..f1b2244 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd @@ -0,0 +1,290 @@ +# holds all decodings for default values +class_name GdDefaultValueDecoder +extends GdUnitSingleton + + +@warning_ignore("unused_parameter") +var _decoders := { + TYPE_NIL: func(value :Variant) -> String: return "null", + TYPE_STRING: func(value :Variant) -> String: return '"%s"' % value, + TYPE_STRING_NAME: _on_type_StringName, + TYPE_BOOL: func(value :Variant) -> String: return str(value).to_lower(), + TYPE_FLOAT: func(value :Variant) -> String: return '%f' % value, + TYPE_COLOR: _on_type_Color, + TYPE_ARRAY: _on_type_Array.bind(TYPE_ARRAY), + TYPE_PACKED_BYTE_ARRAY: _on_type_Array.bind(TYPE_PACKED_BYTE_ARRAY), + TYPE_PACKED_STRING_ARRAY: _on_type_Array.bind(TYPE_PACKED_STRING_ARRAY), + TYPE_PACKED_FLOAT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT32_ARRAY), + TYPE_PACKED_FLOAT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT64_ARRAY), + TYPE_PACKED_INT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT32_ARRAY), + TYPE_PACKED_INT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT64_ARRAY), + TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY), + TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY), + TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY), + TYPE_PACKED_VECTOR4_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR4_ARRAY), + TYPE_DICTIONARY: _on_type_Dictionary, + TYPE_RID: _on_type_RID, + TYPE_NODE_PATH: _on_type_NodePath, + TYPE_VECTOR2: _on_type_Vector.bind(TYPE_VECTOR2), + TYPE_VECTOR2I: _on_type_Vector.bind(TYPE_VECTOR2I), + TYPE_VECTOR3: _on_type_Vector.bind(TYPE_VECTOR3), + TYPE_VECTOR3I: _on_type_Vector.bind(TYPE_VECTOR3I), + TYPE_VECTOR4: _on_type_Vector.bind(TYPE_VECTOR4), + TYPE_VECTOR4I: _on_type_Vector.bind(TYPE_VECTOR4I), + TYPE_RECT2: _on_type_Rect2, + TYPE_RECT2I: _on_type_Rect2i, + TYPE_PLANE: _on_type_Plane, + TYPE_QUATERNION: _on_type_Quaternion, + TYPE_AABB: _on_type_AABB, + TYPE_BASIS: _on_type_Basis, + TYPE_CALLABLE: _on_type_Callable, + TYPE_SIGNAL: _on_type_Signal, + TYPE_TRANSFORM2D: _on_type_Transform2D, + TYPE_TRANSFORM3D: _on_type_Transform3D, + TYPE_PROJECTION: _on_type_Projection, + TYPE_OBJECT: _on_type_Object +} + +static func _regex(pattern: String) -> RegEx: + var regex := RegEx.new() + var err := regex.compile(pattern) + if err != OK: + push_error("error '%s' checked pattern '%s'" % [err, pattern]) + return null + return regex + + +func get_decoder(type: int) -> Callable: + return _decoders.get(type, func(value :Variant) -> String: return '%s' % value) + + +func _on_type_StringName(value: StringName) -> String: + if value.is_empty(): + return 'StringName()' + return 'StringName("%s")' % value + + +func _on_type_Object(value: Variant, _type: int) -> String: + return str(value) + + +func _on_type_Color(color: Color) -> String: + if color == Color.BLACK: + return "Color()" + return "Color%s" % color + + +func _on_type_NodePath(path: NodePath) -> String: + if path.is_empty(): + return 'NodePath()' + return 'NodePath("%s")' % path + + +func _on_type_Callable(_cb: Callable) -> String: + return 'Callable()' + + +func _on_type_Signal(_s: Signal) -> String: + return 'Signal()' + + +func _on_type_Dictionary(dict: Dictionary) -> String: + if dict.is_empty(): + return '{}' + return str(dict) + + +func _on_type_Array(value: Variant, type: int) -> String: + match type: + TYPE_ARRAY: + return str(value) + + TYPE_PACKED_COLOR_ARRAY: + var colors := PackedStringArray() + for color: Color in value: + @warning_ignore("return_value_discarded") + colors.append(_on_type_Color(color)) + if colors.is_empty(): + return "PackedColorArray()" + return "PackedColorArray([%s])" % ", ".join(colors) + + TYPE_PACKED_VECTOR2_ARRAY: + var vectors := PackedStringArray() + for vector: Vector2 in value: + @warning_ignore("return_value_discarded") + vectors.append(_on_type_Vector(vector, TYPE_VECTOR2)) + if vectors.is_empty(): + return "PackedVector2Array()" + return "PackedVector2Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_VECTOR3_ARRAY: + var vectors := PackedStringArray() + for vector: Vector3 in value: + @warning_ignore("return_value_discarded") + vectors.append(_on_type_Vector(vector, TYPE_VECTOR3)) + if vectors.is_empty(): + return "PackedVector3Array()" + return "PackedVector3Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_VECTOR4_ARRAY: + var vectors := PackedStringArray() + for vector: Vector4 in value: + @warning_ignore("return_value_discarded") + vectors.append(_on_type_Vector(vector, TYPE_VECTOR4)) + if vectors.is_empty(): + return "PackedVector4Array()" + return "PackedVector4Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_STRING_ARRAY: + var values := PackedStringArray() + for v: String in value: + @warning_ignore("return_value_discarded") + values.append('"%s"' % v) + if values.is_empty(): + return "PackedStringArray()" + return "PackedStringArray([%s])" % ", ".join(values) + + TYPE_PACKED_BYTE_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY,\ + TYPE_PACKED_FLOAT64_ARRAY,\ + TYPE_PACKED_INT32_ARRAY,\ + TYPE_PACKED_INT64_ARRAY: + var vectors := PackedStringArray() + for vector: Variant in value: + @warning_ignore("return_value_discarded") + vectors.append(str(vector)) + if vectors.is_empty(): + return GdObjects.type_as_string(type) + "()" + return "%s([%s])" % [GdObjects.type_as_string(type), ", ".join(vectors)] + return "unknown array type %d" % type + + +func _on_type_Vector(value: Variant, type: int) -> String: + + if typeof(value) != type: + push_error("Internal Error: type missmatch detected for value '%s', expects type %s" % [value, type_string(type)]) + return "" + + match type: + TYPE_VECTOR2: + if value == Vector2(): + return "Vector2()" + return "Vector2%s" % value + TYPE_VECTOR2I: + if value == Vector2i(): + return "Vector2i()" + return "Vector2i%s" % value + TYPE_VECTOR3: + if value == Vector3(): + return "Vector3()" + return "Vector3%s" % value + TYPE_VECTOR3I: + if value == Vector3i(): + return "Vector3i()" + return "Vector3i%s" % value + TYPE_VECTOR4: + if value == Vector4(): + return "Vector4()" + return "Vector4%s" % value + TYPE_VECTOR4I: + if value == Vector4i(): + return "Vector4i()" + return "Vector4i%s" % value + return "unknown vector type %d" % type + + +func _on_type_Transform2D(transform: Transform2D) -> String: + if transform == Transform2D(): + return "Transform2D()" + return "Transform2D(Vector2%s, Vector2%s, Vector2%s)" % [transform.x, transform.y, transform.origin] + + +func _on_type_Transform3D(transform: Transform3D) -> String: + if transform == Transform3D(): + return "Transform3D()" + return "Transform3D(Vector3%s, Vector3%s, Vector3%s, Vector3%s)" % [transform.basis.x, transform.basis.y, transform.basis.z, transform.origin] + + +func _on_type_Projection(projection: Projection) -> String: + return "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % [projection.x, projection.y, projection.z, projection.w] + + +@warning_ignore("unused_parameter") +func _on_type_RID(value: RID) -> String: + return "RID()" + + +func _on_type_Rect2(rect: Rect2) -> String: + if rect == Rect2(): + return "Rect2()" + return "Rect2(Vector2%s, Vector2%s)" % [rect.position, rect.size] + + +func _on_type_Rect2i(rect: Variant) -> String: + if rect == Rect2i(): + return "Rect2i()" + return "Rect2i(Vector2i%s, Vector2i%s)" % [rect.position, rect.size] + + +func _on_type_Plane(plane: Plane) -> String: + if plane == Plane(): + return "Plane()" + return "Plane(%d, %d, %d, %d)" % [plane.x, plane.y, plane.z, plane.d] + + +func _on_type_Quaternion(quaternion: Quaternion) -> String: + if quaternion == Quaternion(): + return "Quaternion()" + return "Quaternion(%d, %d, %d, %d)" % [quaternion.x, quaternion.y, quaternion.z, quaternion.w] + + +func _on_type_AABB(aabb: AABB) -> String: + if aabb == AABB(): + return "AABB()" + return "AABB(Vector3%s, Vector3%s)" % [aabb.position, aabb.size] + + +func _on_type_Basis(basis: Basis) -> String: + if basis == Basis(): + return "Basis()" + return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z] + + +static func decode(value: Variant) -> String: + var type := typeof(value) + @warning_ignore("unsafe_cast") + if GdArrayTools.is_type_array(type) and (value as Array).is_empty(): + return "" + # For Variant types we need to determine the original type + if type == GdObjects.TYPE_VARIANT: + type = typeof(value) + var decoder := _get_value_decoder(type) + if decoder == null: + push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) + return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) + return decoder.call(value) + + +static func decode_typed(type: int, value: Variant) -> String: + if value == null: + return "null" + # For Variant types we need to determine the original type + if type == GdObjects.TYPE_VARIANT: + type = typeof(value) + var decoder := _get_value_decoder(type) + if decoder == null: + push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) + return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) + return decoder.call(value) + + +static func _get_value_decoder(type: int) -> Callable: + var decoder: GdDefaultValueDecoder = instance( + "GdUnitDefaultValueDecoders", + func() -> GdDefaultValueDecoder: + return GdDefaultValueDecoder.new()) + return decoder.get_decoder(type) diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid new file mode 100644 index 0000000..cf65961 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid @@ -0,0 +1 @@ +uid://dij1gafm2e0hu diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd new file mode 100644 index 0000000..bd38eb2 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd @@ -0,0 +1,208 @@ +class_name GdFunctionArgument +extends RefCounted + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const UNDEFINED: String = "<-NO_ARG->" +const ARG_PARAMETERIZED_TEST := ["test_parameters", "_test_parameters"] + +static var _fuzzer_regex: RegEx +static var _cleanup_leading_spaces: RegEx +static var _fix_comma_space: RegEx + +var _name: String +var _type: int +var _type_hint: int +var _default_value: Variant +var _parameter_sets: PackedStringArray = [] + + +func _init(p_name: String, p_type: int, value: Variant = UNDEFINED, p_type_hint: int = TYPE_NIL) -> void: + _init_static_variables() + _name = p_name + _type = p_type + _type_hint = p_type_hint + if value != null and p_name in ARG_PARAMETERIZED_TEST: + _parameter_sets = _parse_parameter_set(str(value)) + _default_value = value + # is argument a fuzzer? + if _type == TYPE_OBJECT and _fuzzer_regex.search(_name): + _type = GdObjects.TYPE_FUZZER + + +func _init_static_variables() -> void: + if _fuzzer_regex == null: + _fuzzer_regex = GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)") + _cleanup_leading_spaces = RegEx.create_from_string("(?m)^[ \t]+") + _fix_comma_space = RegEx.create_from_string(""", {0,}\t{0,}(?=(?:[^"]*"[^"]*")*[^"]*$)(?!\\s)""") + + +func name() -> String: + return _name + + +func default() -> Variant: + return type_convert(_default_value, _type) + + +func set_value(value: String) -> void: + # we onle need to apply default values for Objects, all others are provided by the method descriptor + if _type == GdObjects.TYPE_FUZZER: + _default_value = value + return + if _name in ARG_PARAMETERIZED_TEST: + _parameter_sets = _parse_parameter_set(value) + _default_value = value + return + + if _type == TYPE_NIL or _type == GdObjects.TYPE_VARIANT: + _type = _extract_value_type(value) + if _type == GdObjects.TYPE_VARIANT and _default_value == null: + _default_value = value + if _default_value == null: + match _type: + TYPE_DICTIONARY: + _default_value = as_dictionary(value) + TYPE_ARRAY: + _default_value = as_array(value) + GdObjects.TYPE_FUZZER: + _default_value = value + _: + _default_value = str_to_var(value) + # if converting fails assign the original value without converting + if _default_value == null and value != null: + _default_value = value + #prints("set default_value: ", _default_value, "with type %d" % _type, " from original: '%s'" % value) + + +func _extract_value_type(value: String) -> int: + if value != UNDEFINED: + if _fuzzer_regex.search(_name): + return GdObjects.TYPE_FUZZER + if value.rfind(")") == value.length()-1: + return GdObjects.TYPE_FUNC + return _type + + +func value_as_string() -> String: + if has_default(): + return GdDefaultValueDecoder.decode_typed(_type, _default_value) + return "" + + +func plain_value() -> Variant: + return _default_value + + +func type() -> int: + return _type + + +func type_hint() -> int: + return _type_hint + + +func has_default() -> bool: + return not is_same(_default_value, UNDEFINED) + + +func is_typed_array() -> bool: + return _type == TYPE_ARRAY and _type_hint != TYPE_NIL + + +func is_parameter_set() -> bool: + return _name in ARG_PARAMETERIZED_TEST + + +func parameter_sets() -> PackedStringArray: + return _parameter_sets + + +static func get_parameter_set(parameters :Array[GdFunctionArgument]) -> GdFunctionArgument: + for current in parameters: + if current != null and current.is_parameter_set(): + return current + return null + + +func _to_string() -> String: + var s := _name + if _type != TYPE_NIL: + s += ": " + GdObjects.type_as_string(_type) + if _type_hint != TYPE_NIL: + s += "[%s]" % GdObjects.type_as_string(_type_hint) + if has_default(): + s += "=" + value_as_string() + return s + + +func _parse_parameter_set(input :String) -> PackedStringArray: + if not input.contains("["): + return [] + + input = _cleanup_leading_spaces.sub(input, "", true) + input = input.replace("\n", "").strip_edges().trim_prefix("[").trim_suffix("]").trim_prefix("]") + var single_quote := false + var double_quote := false + var array_end := 0 + var current_index := 0 + var output :PackedStringArray = [] + var buf := input.to_utf8_buffer() + var collected_characters: = PackedByteArray() + var matched :bool = false + + for c in buf: + current_index += 1 + matched = current_index == buf.size() + @warning_ignore("return_value_discarded") + collected_characters.push_back(c) + + match c: + # ' ': ignore spaces between array elements + 32: if array_end == 0 and (not double_quote and not single_quote): + collected_characters.remove_at(collected_characters.size()-1) + # ',': step over array element seperator ',' + 44: if array_end == 0: + matched = true + collected_characters.remove_at(collected_characters.size()-1) + # '`': + 39: single_quote = !single_quote + # '"': + 34: if not single_quote: double_quote = !double_quote + # '[' + 91: if not double_quote and not single_quote: array_end +=1 # counts array open + # ']' + 93: if not double_quote and not single_quote: array_end -=1 # counts array closed + + # if array closed than collect the element + if matched: + var parameters := _fix_comma_space.sub(collected_characters.get_string_from_utf8(), ", ", true) + if not parameters.is_empty(): + @warning_ignore("return_value_discarded") + output.append(parameters) + collected_characters.clear() + matched = false + return output + + +## value converters + +func as_array(value: String) -> Array: + if value == "Array()" or value == "[]": + return [] + + if value.begins_with("Array("): + value = value.lstrip("Array(").rstrip(")") + if value.begins_with("["): + return str_to_var(value) + return [] + + +func as_dictionary(value: String) -> Dictionary: + if value == "Dictionary()": + return {} + if value.begins_with("Dictionary("): + value = value.lstrip("Dictionary(").rstrip(")") + if value.begins_with("{"): + return str_to_var(value) + return {} diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid new file mode 100644 index 0000000..ddbdf3f --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid @@ -0,0 +1 @@ +uid://ddo8ibsug70g diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd new file mode 100644 index 0000000..d891834 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd @@ -0,0 +1,287 @@ +class_name GdFunctionDescriptor +extends RefCounted + +var _is_virtual :bool +var _is_static :bool +var _is_engine :bool +var _is_coroutine :bool +var _name :String +var _source_path: String +var _line_number :int +var _return_type :int +var _return_class :String +var _args : Array[GdFunctionArgument] +var _varargs :Array[GdFunctionArgument] + + + +static func create(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor: + var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, false, false, p_return_type, "", p_args) + fd.enrich_file_info(p_source_path, p_source_line) + return fd + +static func create_static(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor: + var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, true, false, p_return_type, "", p_args) + fd.enrich_file_info(p_source_path, p_source_line) + return fd + + +func _init(p_name :String, + p_line_number :int, + p_is_virtual :bool, + p_is_static :bool, + p_is_engine :bool, + p_return_type :int, + p_return_class :String, + p_args : Array[GdFunctionArgument], + p_varargs :Array[GdFunctionArgument] = []) -> void: + _name = p_name + _line_number = p_line_number + _return_type = p_return_type + _return_class = p_return_class + _is_virtual = p_is_virtual + _is_static = p_is_static + _is_engine = p_is_engine + _is_coroutine = false + _args = p_args + _varargs = p_varargs + + +func with_return_class(clazz_name: String) -> GdFunctionDescriptor: + _return_class = clazz_name + return self + + +func name() -> String: + return _name + + +func source_path() -> String: + return _source_path + + +func line_number() -> int: + return _line_number + + +func is_virtual() -> bool: + return _is_virtual + + +func is_static() -> bool: + return _is_static + + +func is_engine() -> bool: + return _is_engine + + +func is_vararg() -> bool: + return not _varargs.is_empty() + + +func is_coroutine() -> bool: + return _is_coroutine + + +func is_parameterized() -> bool: + for current in _args: + var arg :GdFunctionArgument = current + if arg.name() in GdFunctionArgument.ARG_PARAMETERIZED_TEST: + return true + return false + + +func is_private() -> bool: + return name().begins_with("_") and not is_virtual() + + +func return_type() -> int: + return _return_type + + +func return_type_as_string() -> String: + if return_type() == TYPE_NIL: + return "void" + if (return_type() == TYPE_OBJECT or return_type() == GdObjects.TYPE_ENUM) and not _return_class.is_empty(): + return _return_class + return GdObjects.type_as_string(return_type()) + + +func set_argument_value(arg_name: String, value: String) -> void: + var argument: GdFunctionArgument = _args.filter(func(arg: GdFunctionArgument) -> bool: + return arg.name() == arg_name + ).front() + if argument != null: + argument.set_value(value) + + +func enrich_arguments(arguments: Array[Dictionary]) -> void: + for arg_index: int in arguments.size(): + var arg: Dictionary = arguments[arg_index] + if arg["type"] != GdObjects.TYPE_VARARG: + var arg_name: String = arg["name"] + var arg_value: String = arg["value"] + set_argument_value(arg_name, arg_value) + + +func enrich_file_info(p_source_path: String, p_line_number: int) -> void: + _source_path = p_source_path + _line_number = p_line_number + + +func args() -> Array[GdFunctionArgument]: + return _args + + +func varargs() -> Array[GdFunctionArgument]: + return _varargs + + +func typed_args() -> String: + var collect := PackedStringArray() + for arg in args(): + @warning_ignore("return_value_discarded") + collect.push_back(arg._to_string()) + for arg in varargs(): + @warning_ignore("return_value_discarded") + collect.push_back(arg._to_string()) + return ", ".join(collect) + + +func _to_string() -> String: + var fsignature := "virtual " if is_virtual() else "" + if _return_type == TYPE_NIL: + return fsignature + "[Line:%s] func %s(%s):" % [line_number(), name(), typed_args()] + var func_template := fsignature + "[Line:%s] func %s(%s) -> %s:" + if is_static(): + func_template= "[Line:%s] static func %s(%s) -> %s:" + return func_template % [line_number(), name(), typed_args(), return_type_as_string()] + + +# extract function description given by Object.get_method_list() +static func extract_from(descriptor :Dictionary, is_engine_ := true) -> GdFunctionDescriptor: + var func_name: String = descriptor["name"] + var function_flags: int = descriptor["flags"] + var return_descriptor: Dictionary = descriptor["return"] + var clazz_name: String = return_descriptor["class_name"] + var is_virtual_: bool = function_flags & METHOD_FLAG_VIRTUAL + var is_static_: bool = function_flags & METHOD_FLAG_STATIC + var is_vararg_: bool = function_flags & METHOD_FLAG_VARARG + + var return_type_ := _extract_return_type(return_descriptor) + return GdFunctionDescriptor.new( + func_name, + -1, + is_virtual_, + is_static_, + is_engine_, + return_type_, + clazz_name, + _extract_args(descriptor), + _build_varargs(is_vararg_) + ) + +# temporary exclude GlobalScope enums +const enum_fix := [ + "Side", + "Corner", + "Orientation", + "ClockDirection", + "HorizontalAlignment", + "VerticalAlignment", + "InlineAlignment", + "EulerOrder", + "Error", + "Key", + "MIDIMessage", + "MouseButton", + "MouseButtonMask", + "JoyButton", + "JoyAxis", + "PropertyHint", + "PropertyUsageFlags", + "MethodFlags", + "Variant.Type", + "Control.LayoutMode"] + + +static func _extract_return_type(return_info :Dictionary) -> int: + var type :int = return_info["type"] + var usage :int = return_info["usage"] + if usage & PROPERTY_USAGE_CLASS_IS_ENUM: + return GdObjects.TYPE_ENUM + if usage & PROPERTY_USAGE_NIL_IS_VARIANT: + return GdObjects.TYPE_VARIANT + if type == TYPE_NIL: + return GdObjects.TYPE_VOID + return type + + +static func _extract_args(descriptor :Dictionary) -> Array[GdFunctionArgument]: + var args_ :Array[GdFunctionArgument] = [] + var arguments :Array = descriptor["args"] + var defaults :Array = descriptor["default_args"] + # iterate backwards because the default values are stored from right to left + while not arguments.is_empty(): + var arg :Dictionary = arguments.pop_back() + var arg_name := _argument_name(arg) + var arg_type := _argument_type(arg) + var arg_type_hint := _argument_hint(arg) + #var arg_class: StringName = arg["class_name"] + var default_value: Variant = GdFunctionArgument.UNDEFINED if defaults.is_empty() else defaults.pop_back() + args_.push_front(GdFunctionArgument.new(arg_name, arg_type, default_value, arg_type_hint)) + return args_ + + +static func _build_varargs(p_is_vararg :bool) -> Array[GdFunctionArgument]: + var varargs_ :Array[GdFunctionArgument] = [] + if not p_is_vararg: + return varargs_ + varargs_.push_back(GdFunctionArgument.new("varargs", GdObjects.TYPE_VARARG, '')) + return varargs_ + + +static func _argument_name(arg :Dictionary) -> String: + return arg["name"] + + +static func _argument_type(arg :Dictionary) -> int: + var type :int = arg["type"] + var usage :int = arg["usage"] + + if type == TYPE_OBJECT: + if arg["class_name"] == "Node": + return GdObjects.TYPE_NODE + if arg["class_name"] == "Fuzzer": + return GdObjects.TYPE_FUZZER + + # if the argument untyped we need to scan the assignef value type + if type == TYPE_NIL and usage == PROPERTY_USAGE_NIL_IS_VARIANT: + return GdObjects.TYPE_VARIANT + return type + + +static func _argument_hint(arg :Dictionary) -> int: + var hint :int = arg["hint"] + var hint_string :String = arg["hint_string"] + + match hint: + PROPERTY_HINT_ARRAY_TYPE: + return GdObjects.string_to_type(hint_string) + _: + return 0 + + +static func _argument_type_as_string(arg :Dictionary) -> String: + var type := _argument_type(arg) + match type: + TYPE_NIL: + return "" + TYPE_OBJECT: + var clazz_name :String = arg["class_name"] + if not clazz_name.is_empty(): + return clazz_name + return "" + _: + return GdObjects.type_as_string(type) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid new file mode 100644 index 0000000..20b6f3e --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid @@ -0,0 +1 @@ +uid://d2p7fx2v16d4t diff --git a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd new file mode 100644 index 0000000..9c45e62 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd @@ -0,0 +1,188 @@ +class_name GdFunctionParameterSetResolver +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ParameterExtractor extends '${clazz_path}' + +func __extract_test_parameters() -> Array: + return ${test_params} + +""" + +const EXCLUDE_PROPERTIES_TO_COPY = [ + "script", + "type", + "Node", + "_import_path"] + + +var _fd: GdFunctionDescriptor +var _static_sets_by_index := {} +var _is_static := true + +func _init(fd: GdFunctionDescriptor) -> void: + _fd = fd + + +func resolve_test_cases(script: GDScript) -> Array[GdUnitTestCase]: + if not is_parameterized(): + return [GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name())] + return extract_test_cases_by_reflection(script) + + +func is_parameterized() -> bool: + return _fd.is_parameterized() + + +func is_parameter_sets_static() -> bool: + return _is_static + + +func is_parameter_set_static(index: int) -> bool: + return _is_static and _static_sets_by_index.get(index, false) + + +# validates the given arguments are complete and matches to required input fields of the test function +func validate(input_value_set: Array) -> String: + var input_arguments := _fd.args() + # check given parameter set with test case arguments + var expected_arg_count := input_arguments.size() - 1 + for input_values :Variant in input_value_set: + var parameter_set_index := input_value_set.find(input_values) + if input_values is Array: + var arr_values: Array = input_values + var current_arg_count := arr_values.size() + if current_arg_count != expected_arg_count: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count] + var error := validate_parameter_types(input_arguments, arr_values, parameter_set_index) + if not error.is_empty(): + return error + else: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n Expecting an array of input values." % parameter_set_index + return "" + + +static func validate_parameter_types(input_arguments: Array, input_values: Array, parameter_set_index: int) -> String: + for i in input_arguments.size(): + var input_param: GdFunctionArgument = input_arguments[i] + # only check the test input arguments + if input_param.is_parameter_set(): + continue + var input_param_type := input_param.type() + var input_value :Variant = input_values[i] + var input_value_type := typeof(input_value) + # input parameter is not typed or is Variant we skip the type test + if input_param_type == TYPE_NIL or input_param_type == GdObjects.TYPE_VARIANT: + continue + # is input type enum allow int values + if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT: + continue + # allow only equal types and object == null + if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL: + continue + if input_param_type != input_value_type: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The value '%s' does not match the required input parameter <%s>." % [parameter_set_index, input_value, input_param] + return "" + + +func extract_test_cases_by_reflection(script: GDScript) -> Array[GdUnitTestCase]: + var source: Node = script.new() + source.queue_free() + + var fa := GdFunctionArgument.get_parameter_set(_fd.args()) + var parameter_sets := fa.parameter_sets() + # if no parameter set detected we need to resolve it by using reflection + if parameter_sets.size() == 0: + _is_static = false + return _extract_test_cases_by_reflection(source, script) + else: + var test_cases: Array[GdUnitTestCase] = [] + var property_names := _extract_property_names(source) + for parameter_set_index in parameter_sets.size(): + var parameter_set := parameter_sets[parameter_set_index] + _static_sets_by_index[parameter_set_index] = _is_static_parameter_set(parameter_set, property_names) + @warning_ignore("return_value_discarded") + test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), parameter_set_index, parameter_set)) + parameter_set_index += 1 + return test_cases + + +func _extract_property_names(source: Node) -> PackedStringArray: + return source.get_property_list()\ + .map(func(property :Dictionary) -> String: return property["name"])\ + .filter(func(property :String) -> bool: return !EXCLUDE_PROPERTIES_TO_COPY.has(property)) + + +# tests if the test property set contains an property reference by name, if not the parameter set holds only static values +func _is_static_parameter_set(parameters :String, property_names :PackedStringArray) -> bool: + for property_name in property_names: + if parameters.contains(property_name): + _is_static = false + return false + return true + + +func _extract_test_cases_by_reflection(source: Node, script: GDScript) -> Array[GdUnitTestCase]: + var parameter_sets := load_parameter_sets(source) + var test_cases: Array[GdUnitTestCase] = [] + for index in parameter_sets.size(): + var parameter_set := str(parameter_sets[index]) + @warning_ignore("return_value_discarded") + test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), index, parameter_set)) + return test_cases + + +# extracts the arguments from the given test case, using kind of reflection solution +# to restore the parameters from a string representation to real instance type +func load_parameter_sets(source: Node) -> Array: + var source_script: GDScript = source.get_script() + var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args()) + var source_code := CLASS_TEMPLATE \ + .replace("${clazz_path}", source_script.resource_path) \ + .replace("${test_params}", parameter_arg.value_as_string()) + var script := GDScript.new() + script.source_code = source_code + # enable this lines only for debuging + #script.resource_path = GdUnitFileAccess.create_temp_dir("parameter_extract") + "/%s__.gd" % test_case.get_name() + #DirAccess.remove_absolute(script.resource_path) + #ResourceSaver.save(script, script.resource_path) + var result := script.reload() + if result != OK: + push_error("Extracting test parameters failed! Script loading error: %s" % result) + return [] + var instance: Node = script.new() + GdFunctionParameterSetResolver.copy_properties(source, instance) + instance.queue_free() + var parameter_sets: Array = instance.call("__extract_test_parameters") + return fixure_typed_parameters(parameter_sets, _fd.args()) + + +func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array: + for parameter_set_index in parameter_sets.size(): + var parameter_set: Array = parameter_sets[parameter_set_index] + # run over all function arguments + for parameter_index in parameter_set.size(): + var parameter :Variant = parameter_set[parameter_index] + var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index] + if parameter is Array: + var as_array: Array = parameter + # we need to convert the untyped array to the expected typed version + if arg_descriptor.is_typed_array(): + parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null) + return parameter_sets + + +static func copy_properties(source: Object, dest: Object) -> void: + for property in source.get_property_list(): + var property_name :String = property["name"] + var property_value :Variant = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid new file mode 100644 index 0000000..681260f --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid @@ -0,0 +1 @@ +uid://cp87n2tcy0lvk diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd b/addons/gdUnit4/src/core/parse/GdScriptParser.gd new file mode 100644 index 0000000..5dd293c --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -0,0 +1,766 @@ +class_name GdScriptParser +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const TYPE_VOID = GdObjects.TYPE_VOID +const TYPE_VARIANT = GdObjects.TYPE_VARIANT +const TYPE_VARARG = GdObjects.TYPE_VARARG +const TYPE_FUNC = GdObjects.TYPE_FUNC +const TYPE_FUZZER = GdObjects.TYPE_FUZZER +const TYPE_ENUM = GdObjects.TYPE_ENUM + + +var TOKEN_NOT_MATCH := Token.new("") +var TOKEN_SPACE := SkippableToken.new(" ") +var TOKEN_TABULATOR := SkippableToken.new("\t") +var TOKEN_NEW_LINE := SkippableToken.new("\n") +var TOKEN_COMMENT := SkippableToken.new("#") +var TOKEN_CLASS_NAME := RegExToken.new("class_name", GdUnitTools.to_regex("(class_name)\\s+([\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class_name)\\s+([\\w\\p{L}\\p{N}_]+)"), 5) +var TOKEN_INNER_CLASS := TokenInnerClass.new("class", GdUnitTools.to_regex("(class)\\s+(\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class)\\s+([\\w\\p{L}\\p{N}_]+)"), 5) +var TOKEN_EXTENDS := RegExToken.new("extends", GdUnitTools.to_regex("extends\\s+")) +var TOKEN_ENUM := RegExToken.new("enum", GdUnitTools.to_regex("enum\\s+")) +var TOKEN_FUNCTION_STATIC_DECLARATION := RegExToken.new("static func", GdUnitTools.to_regex("^static\\s+func\\s+([\\w\\p{L}\\p{N}_]+)"), 1) +var TOKEN_FUNCTION_DECLARATION := RegExToken.new("func", GdUnitTools.to_regex("^func\\s+([\\w\\p{L}\\p{N}_]+)"), 1) +var TOKEN_FUNCTION := Token.new(".") +var TOKEN_FUNCTION_RETURN_TYPE := Token.new("->") +var TOKEN_FUNCTION_END := Token.new("):") +var TOKEN_ARGUMENT_ASIGNMENT := Token.new("=") +var TOKEN_ARGUMENT_TYPE_ASIGNMENT := Token.new(":=") +var TOKEN_ARGUMENT_FUZZER := FuzzerToken.new(GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)")) +var TOKEN_ARGUMENT_TYPE := Token.new(":") +var TOKEN_ARGUMENT_VARIADIC := Token.new("...") +var TOKEN_ARGUMENT_SEPARATOR := Token.new(",") +var TOKEN_BRACKET_ROUND_OPEN := Token.new("(") +var TOKEN_BRACKET_ROUND_CLOSE := Token.new(")") +var TOKEN_BRACKET_SQUARE_OPEN := Token.new("[") +var TOKEN_BRACKET_SQUARE_CLOSE := Token.new("]") +var TOKEN_BRACKET_CURLY_OPEN := Token.new("{") +var TOKEN_BRACKET_CURLY_CLOSE := Token.new("}") +var TOKEN_BACKSLASH := Token.new("\\") + + +var OPERATOR_ADD := Operator.new("+") +var OPERATOR_SUB := Operator.new("-") +var OPERATOR_MUL := Operator.new("*") +var OPERATOR_DIV := Operator.new("/") +var OPERATOR_REMAINDER := Operator.new("%") + +var TOKENS :Array[Token] = [ + TOKEN_SPACE, + TOKEN_TABULATOR, + TOKEN_NEW_LINE, + TOKEN_BACKSLASH, + TOKEN_COMMENT, + TOKEN_BRACKET_ROUND_OPEN, + TOKEN_BRACKET_ROUND_CLOSE, + TOKEN_BRACKET_SQUARE_OPEN, + TOKEN_BRACKET_SQUARE_CLOSE, + TOKEN_BRACKET_CURLY_OPEN, + TOKEN_BRACKET_CURLY_CLOSE, + TOKEN_CLASS_NAME, + TOKEN_INNER_CLASS, + TOKEN_EXTENDS, + TOKEN_ENUM, + TOKEN_FUNCTION_STATIC_DECLARATION, + TOKEN_FUNCTION_DECLARATION, + TOKEN_ARGUMENT_FUZZER, + TOKEN_ARGUMENT_TYPE_ASIGNMENT, + TOKEN_ARGUMENT_ASIGNMENT, + TOKEN_ARGUMENT_TYPE, + TOKEN_ARGUMENT_VARIADIC, + TOKEN_FUNCTION, + TOKEN_ARGUMENT_SEPARATOR, + TOKEN_FUNCTION_RETURN_TYPE, + OPERATOR_ADD, + OPERATOR_SUB, + OPERATOR_MUL, + OPERATOR_DIV, + OPERATOR_REMAINDER, +] + +var _regex_strip_comments := GdUnitTools.to_regex("^([^#\"']|'[^']*'|\"[^\"]*\")*\\K#.*") +var _scanned_inner_classes := PackedStringArray() +var _script_constants := {} +var _is_awaiting := GdUnitTools.to_regex("\\bawait\\s+(?![^\"]*\"[^\"]*$)(?!.*#.*await)") + + +static func to_unix_format(input :String) -> String: + return input.replace("\r\n", "\n") + + +class Token extends RefCounted: + var _token: String + var _consumed: int + var _is_operator: bool + + func _init(p_token: String, p_is_operator := false) -> void: + _token = p_token + _is_operator = p_is_operator + _consumed = p_token.length() + + func match(input: String, pos: int) -> bool: + return input.findn(_token, pos) == pos + + func value() -> Variant: + return _token + + func is_operator() -> bool: + return _is_operator + + func is_inner_class() -> bool: + return _token == "class" + + func is_variable() -> bool: + return false + + func is_token(token_name :String) -> bool: + return _token == token_name + + func is_skippable() -> bool: + return false + + func _to_string() -> String: + return "Token{" + _token + "}" + + +class Operator extends Token: + func _init(p_value: String) -> void: + super(p_value, true) + + func _to_string() -> String: + return "OperatorToken{%s}" % [_token] + + +# A skippable token, is just a placeholder like space or tabs +class SkippableToken extends Token: + + func _init(p_token: String) -> void: + super(p_token) + + func is_skippable() -> bool: + return true + + +# Token to parse function arguments +class Variable extends Token: + var _plain_value :String + var _typed_value :Variant + var _type :int = TYPE_NIL + + + func _init(p_value: String) -> void: + super(p_value) + _type = _scan_type(p_value) + _plain_value = p_value + _typed_value = _cast_to_type(p_value, _type) + + + func _scan_type(p_value: String) -> int: + if p_value.begins_with("\"") and p_value.ends_with("\""): + return TYPE_STRING + var type_ := GdObjects.string_to_type(p_value) + if type_ != TYPE_NIL: + return type_ + if p_value.is_valid_int(): + return TYPE_INT + if p_value.is_valid_float(): + return TYPE_FLOAT + if p_value.is_valid_hex_number(): + return TYPE_INT + return TYPE_OBJECT + + + func _cast_to_type(p_value :String, p_type: int) -> Variant: + match p_type: + TYPE_STRING: + return p_value#.substr(1, p_value.length() - 2) + TYPE_INT: + return p_value.to_int() + TYPE_FLOAT: + return p_value.to_float() + return p_value + + + func is_variable() -> bool: + return true + + + func type() -> int: + return _type + + + func value() -> Variant: + return _typed_value + + + func plain_value() -> String: + return _plain_value + + + func _to_string() -> String: + return "Variable{%s: %s : '%s'}" % [_plain_value, GdObjects.type_as_string(_type), _token] + + +class RegExToken extends Token: + var _regex: RegEx + var _extract_group_index: int + var _value := "" + + + func _init(token: String, regex: RegEx, extract_group_index: int = -1) -> void: + super(token, false) + _regex = regex + _extract_group_index = extract_group_index + + + func match(input: String, pos: int) -> bool: + var matching := _regex.search(input, pos) + if matching == null or pos != matching.get_start(): + return false + if _extract_group_index != -1: + _value = matching.get_string(_extract_group_index) + _consumed = matching.get_end() - matching.get_start() + return true + + + func value() -> String: + return _value + + +# Token to parse Fuzzers +class FuzzerToken extends RegExToken: + + + func _init(regex: RegEx) -> void: + super("fuzzer", regex, 1) + + + func name() -> String: + return value() + + + func type() -> int: + return GdObjects.TYPE_FUZZER + + + func _to_string() -> String: + return "FuzzerToken{%s: '%s'}" % [value(), _token] + + +class TokenInnerClass extends RegExToken: + var _content := PackedStringArray() + + + static func _strip_leading_spaces(input: String) -> String: + var characters := input.to_utf8_buffer() + while not characters.is_empty(): + if characters[0] != 0x20: + break + characters.remove_at(0) + return characters.get_string_from_utf8() + + + static func _consumed_bytes(row: String) -> int: + return row.replace(" ", "").replace(" ", "").length() + + + func _init(token: String, p_regex: RegEx, extract_group_index: int = -1) -> void: + super(token, p_regex, extract_group_index) + + + func is_class_name(clazz_name: String) -> bool: + return value() == clazz_name + + + func content() -> PackedStringArray: + return _content + + + @warning_ignore_start("return_value_discarded") + func parse(source_rows: PackedStringArray, offset: int) -> void: + # add class signature + _content.clear() + _content.append(source_rows[offset]) + # parse class content + for row_index in range(offset+1, source_rows.size()): + # scan until next non tab + var source_row := source_rows[row_index] + var row := TokenInnerClass._strip_leading_spaces(source_row) + if row.is_empty() or row.begins_with("\t") or row.begins_with("#"): + # fold all line to left by removing leading tabs and spaces + if source_row.begins_with("\t"): + source_row = source_row.trim_prefix("\t") + # refomat invalid empty lines + if source_row.dedent().is_empty(): + _content.append("") + else: + _content.append(source_row) + continue + break + _consumed += TokenInnerClass._consumed_bytes("".join(_content)) + @warning_ignore_restore("return_value_discarded") + + + func _to_string() -> String: + return "TokenInnerClass{%s}" % [value()] + + + +func get_token(input: String, current_index: int) -> Token: + for t in TOKENS: + if t.match(input, current_index): + return t + return TOKEN_NOT_MATCH + + +func next_token(input: String, current_index: int, ignore_tokens :Array[Token] = []) -> Token: + var token := TOKEN_NOT_MATCH + for t :Token in TOKENS.filter(func(t :Token) -> bool: return not ignore_tokens.has(t)): + + if t.match(input, current_index): + token = t + break + if token == OPERATOR_SUB: + token = tokenize_value(input, current_index, token) + if token == TOKEN_NOT_MATCH: + return tokenize_value(input, current_index, token, ignore_tokens.has(TOKEN_FUNCTION)) + return token + + +func tokenize_value(input: String, current: int, token: Token, ignore_dots := false) -> Token: + var next := 0 + var current_token := "" + # test for '--', '+-', '*-', '/-', '%-', or at least '-x' + var test_for_sign := (token == null or token.is_operator()) and input[current] == "-" + while current + next < len(input): + var character := input[current + next] as String + # if first charater a sign + # or allowend charset + # or is a float value + if (test_for_sign and next==0) \ + or is_allowed_character(character) \ + or (character == "." and (ignore_dots or current_token.is_valid_int())): + current_token += character + next += 1 + continue + break + if current_token != "": + return Variable.new(current_token) + return TOKEN_NOT_MATCH + + +# const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"" +func is_allowed_character(input: String) -> bool: + var code_point := input.unicode_at(0) + # Unicode + if code_point > 127: + # This is a Unicode character (Chinese, Japanese, etc.) + return true + # ASCII digit 0-9 + if code_point >= 48 and code_point <= 57: + return true + # ASCII lowercase a-z + if code_point >= 97 and code_point <= 122: + return true + # ASCII uppercase A-Z + if code_point >= 65 and code_point <= 90: + return true + # underscore _ + if code_point == 95: + return true + # quotes '" + if code_point == 34 or code_point == 39: + return true + return false + + +func parse_return_token(input: String) -> Variable: + var index := input.rfind(TOKEN_FUNCTION_RETURN_TYPE._token) + if index == -1: + return TOKEN_NOT_MATCH + index += TOKEN_FUNCTION_RETURN_TYPE._consumed + # We scan for the return value exclusive '.' token because it could be referenced to a + # external or internal class e.g. 'func foo() -> InnerClass.Bar:' + var token := next_token(input, index, [TOKEN_FUNCTION]) + while !token.is_variable() and token != TOKEN_NOT_MATCH: + index += token._consumed + token = next_token(input, index, [TOKEN_FUNCTION]) + return token + + +func get_function_descriptors(script: GDScript, included_functions: PackedStringArray = []) -> Array[GdFunctionDescriptor]: + var fds: Array[GdFunctionDescriptor] = [] + for method_descriptor in script.get_script_method_list(): + var func_name: String = method_descriptor["name"] + if included_functions.is_empty() or func_name in included_functions: + # exclude type set/geters + if is_getter_or_setter(func_name): + continue + if not fds.any(func(fd: GdFunctionDescriptor) -> bool: return fd.name() == func_name): + fds.append(GdFunctionDescriptor.extract_from(method_descriptor, false)) + + # we need to enrich it by default arguments and line number by parsing the script + # the engine core functions has no valid methods to get this info + _prescan_script(script) + _enrich_function_descriptor(script, fds) + return fds + + +func is_getter_or_setter(func_name: String) -> bool: + return func_name.begins_with("@") and (func_name.ends_with("getter") or func_name.ends_with("setter")) + + +func _parse_function_arguments(input: String) -> Array[Dictionary]: + var arguments: Array[Dictionary] = [] + var current_index := 0 + var token: Token = null + var bracket := 0 + var in_function := false + + + while current_index < len(input): + token = next_token(input, current_index) + # fallback to not end in a endless loop + if token == TOKEN_NOT_MATCH: + var error := """ + Parsing Error: Invalid token at pos %d found. + Please report this error! + source_code: + -------------------------------------------------------------- + %s + -------------------------------------------------------------- + """.dedent() % [current_index, input] + push_error(error) + current_index += 1 + continue + current_index += token._consumed + if token.is_skippable(): + continue + if token == TOKEN_BRACKET_ROUND_OPEN : + in_function = true + bracket += 1 + if token == TOKEN_BRACKET_ROUND_CLOSE: + bracket -= 1 + # if function end? + if in_function and bracket == 0: + return arguments + # is function + if token == TOKEN_FUNCTION_DECLARATION: + continue + + # is value argument + if in_function: + var arg_value := "" + var current_argument := { + "name" : "", + "value" : GdFunctionArgument.UNDEFINED, + "type" : TYPE_VARIANT + } + + # parse type and default value + while current_index < len(input): + token = next_token(input, current_index) + current_index += token._consumed + if token.is_skippable(): + continue + + if token.is_variable() && current_argument["name"] == "": + arguments.append(current_argument) + current_argument["name"] = (token as Variable).plain_value() + continue + + match token: + # is fuzzer argument + TOKEN_ARGUMENT_FUZZER: + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + current_argument["name"] = (token as FuzzerToken).name() + current_argument["value"] = arg_value.lstrip(" ") + current_argument["type"] = TYPE_FUZZER + arguments.append(current_argument) + continue + + TOKEN_ARGUMENT_VARIADIC: + current_argument["type"] = TYPE_VARARG + + TOKEN_ARGUMENT_TYPE: + token = next_token(input, current_index) + if token == TOKEN_SPACE: + current_index += token._consumed + token = next_token(input, current_index) + current_index += token._consumed + if current_argument["type"] != TYPE_VARARG: + current_argument["type"] = GdObjects.string_to_type((token as Variable).plain_value()) + + TOKEN_ARGUMENT_TYPE_ASIGNMENT: + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + current_argument["value"] = arg_value.lstrip(" ") + TOKEN_ARGUMENT_ASIGNMENT: + token = next_token(input, current_index) + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + current_argument["value"] = arg_value.lstrip(" ") + + TOKEN_BRACKET_SQUARE_OPEN: + bracket += 1 + TOKEN_BRACKET_CURLY_OPEN: + bracket += 1 + TOKEN_BRACKET_ROUND_OPEN : + bracket += 1 + # if value a function? + if bracket > 1: + # complete the argument value + var func_begin := input.substr(current_index-TOKEN_BRACKET_ROUND_OPEN ._consumed) + var func_body := _parse_end_function(func_begin) + arg_value += func_body + # fix parse index to end of value + current_index += func_body.length() - TOKEN_BRACKET_ROUND_OPEN ._consumed - TOKEN_BRACKET_ROUND_CLOSE._consumed + TOKEN_BRACKET_SQUARE_CLOSE: + bracket -= 1 + TOKEN_BRACKET_CURLY_CLOSE: + bracket -= 1 + TOKEN_BRACKET_ROUND_CLOSE: + bracket -= 1 + # end of function + if bracket == 0: + break + TOKEN_ARGUMENT_SEPARATOR: + if bracket <= 1: + # next argument + current_argument = { + "name" : "", + "value" : GdFunctionArgument.UNDEFINED, + "type" : GdObjects.TYPE_VARIANT + } + continue + return arguments + + +func _parse_end_function(input: String, remove_trailing_char := false) -> String: + # find end of function + var current_index := 0 + var bracket_count := 0 + var in_array := 0 + var in_dict := 0 + var end_of_func := false + + while current_index < len(input) and not end_of_func: + var character := input[current_index] + # step over strings + if character == "'" : + current_index = input.find("'", current_index+1) + 1 + if current_index == 0: + push_error("Parsing error on '%s', can't evaluate end of string." % input) + return "" + continue + if character == '"' : + # test for string blocks + if input.find('"""', current_index) == current_index: + current_index = input.find('"""', current_index+3) + 3 + else: + current_index = input.find('"', current_index+1) + 1 + if current_index == 0: + push_error("Parsing error on '%s', can't evaluate end of string." % input) + return "" + continue + + match character: + # count if inside an array + "[": in_array += 1 + "]": in_array -= 1 + # count if inside an dictionary + "{": in_dict += 1 + "}": in_dict -= 1 + # count if inside a function + "(": bracket_count += 1 + ")": + bracket_count -= 1 + if bracket_count < 0 and in_array <= 0 and in_dict <= 0: + end_of_func = true + ",": + if bracket_count == 0 and in_array == 0 and in_dict <= 0: + end_of_func = true + current_index += 1 + if remove_trailing_char: + # check if the parsed value ends with comma or end of doubled breaked + # `,` or `())` + var trailing_char := input[current_index-1] + if trailing_char == ',' or (bracket_count < 0 and trailing_char == ')'): + return input.substr(0, current_index-1) + return input.substr(0, current_index) + + +func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> PackedStringArray: + for row_index in source_rows.size(): + var input := source_rows[row_index] + var token := next_token(input, 0) + if token.is_inner_class(): + @warning_ignore("unsafe_method_access") + if token.is_class_name(clazz_name): + @warning_ignore("unsafe_method_access") + token.parse(source_rows, row_index) + @warning_ignore("unsafe_method_access") + return token.content() + return PackedStringArray() + + +func extract_func_signature(rows: PackedStringArray, index: int) -> String: + var signature := "" + + for rowIndex in range(index, rows.size()): + var row := rows[rowIndex] + row = _regex_strip_comments.sub(row, "").strip_edges(false) + if row.is_empty(): + continue + signature += row + "\n" + if is_func_end(row): + return signature.strip_edges() + push_error("Can't fully extract function signature of '%s'" % rows[index]) + return "" + + +func get_class_name(script :GDScript) -> String: + var source_code := GdScriptParser.to_unix_format(script.source_code) + var source_rows := source_code.split("\n") + + for index :int in min(10, source_rows.size()): + var input := source_rows[index] + var token := next_token(input, 0) + if token == TOKEN_CLASS_NAME: + return token.value() + # if no class_name found extract from file name + return GdObjects.to_pascal_case(script.resource_path.get_basename().get_file()) + + +func parse_func_name(input: String) -> String: + if TOKEN_FUNCTION_DECLARATION.match(input, 0): + return TOKEN_FUNCTION_DECLARATION.value() + if TOKEN_FUNCTION_STATIC_DECLARATION.match(input, 0): + return TOKEN_FUNCTION_STATIC_DECLARATION.value() + push_error("Can't extract function name from '%s'" % input) + return "" + + +## Enriches the function descriptor by line number and argument default values +## - enrich all function descriptors form current script up to all inherited scrips +func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescriptor]) -> void: + var enriched_functions := {} # Use Dictionary for O(1) lookup instead of PackedStringArray + var script_to_scan := script + while script_to_scan != null: + # do not scan the test suite base class itself + if script_to_scan.resource_path == "res://addons/gdUnit4/src/GdUnitTestSuite.gd": + break + + var rows := script_to_scan.source_code.split("\n") + for rowIndex in rows.size(): + var input := rows[rowIndex] + # step over inner class functions + if input.begins_with("\t"): + continue + # skip comments and empty lines + if input.begins_with("#") or input.length() == 0: + continue + var token := next_token(input, 0) + if token != TOKEN_FUNCTION_STATIC_DECLARATION and token != TOKEN_FUNCTION_DECLARATION: + continue + + var function_name: String = token.value() + # Skip if already enriched (from parent class scan) + if enriched_functions.has(function_name): + continue + + # Find matching function descriptor + var fd: GdFunctionDescriptor = null + for candidate in fds: + if candidate.name() == function_name: + fd = candidate + break + if fd == null: + continue + # Mark as enriched + enriched_functions[function_name] = true + var func_signature := extract_func_signature(rows, rowIndex) + var func_arguments := _parse_function_arguments(func_signature) + # enrich missing default values + fd.enrich_arguments(func_arguments) + fd.enrich_file_info(script_to_scan.resource_path, rowIndex + 1) + fd._is_coroutine = is_func_coroutine(rows, rowIndex) + # enrich return class name if not set + if fd.return_type() == TYPE_OBJECT and fd._return_class in ["", "Resource", "RefCounted"]: + var var_token := parse_return_token(func_signature) + if var_token != TOKEN_NOT_MATCH and var_token.type() == TYPE_OBJECT: + fd._return_class = _patch_inner_class_names(var_token.plain_value(), "") + # if the script ihnerits we need to scan this also + script_to_scan = script_to_scan.get_base_script() + + +func is_func_coroutine(rows :PackedStringArray, index :int) -> bool: + var is_coroutine := false + for rowIndex in range(index+1, rows.size()): + var input := rows[rowIndex].strip_edges() + if input.begins_with("#") or input.is_empty(): + continue + var token := next_token(input, 0) + # scan until next function + if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: + break + + if _is_awaiting.search(input): + return true + return is_coroutine + + +func is_inner_class(clazz_path :PackedStringArray) -> bool: + return clazz_path.size() > 1 + + +func is_func_end(row :String) -> bool: + return row.strip_edges(false, true).ends_with(":") + + +func _patch_inner_class_names(clazz :String, clazz_name :String = "") -> String: + var inner_clazz_name := clazz.split(".")[0] + if _scanned_inner_classes.has(inner_clazz_name): + return inner_clazz_name + #var base_clazz := clazz_name.split(".")[0] + #return base_clazz + "." + clazz + if _script_constants.has(clazz): + return clazz_name + "." + clazz + return clazz + + +func _prescan_script(script: GDScript) -> void: + _script_constants = script.get_script_constant_map() + for key :String in _script_constants.keys(): + var value :Variant = _script_constants.get(key) + if value is GDScript: + @warning_ignore("return_value_discarded") + _scanned_inner_classes.append(key) + + +func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult: + if clazz_path.is_empty(): + return GdUnitResult.error("Invalid script path '%s'" % clazz_path) + var is_inner_class_ := is_inner_class(clazz_path) + var script :GDScript = load(clazz_path[0]) + _prescan_script(script) + + if is_inner_class_: + var inner_class_name := clazz_path[1] + if _scanned_inner_classes.has(inner_class_name): + # do load only on inner class source code and enrich the stored script instance + var source_code := _load_inner_class(script, inner_class_name) + script = _script_constants.get(inner_class_name) + script.source_code = source_code + var function_descriptors := get_function_descriptors(script) + var gd_class := GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors) + return GdUnitResult.success(gd_class) + + +func _load_inner_class(script: GDScript, inner_clazz: String) -> String: + var source_rows := GdScriptParser.to_unix_format(script.source_code).split("\n") + # extract all inner class names + var inner_class_code := extract_inner_class(source_rows, inner_clazz) + return "\n".join(inner_class_code) diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid b/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid new file mode 100644 index 0000000..aceb892 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid @@ -0,0 +1 @@ +uid://cv1eyssspvlk6 diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd new file mode 100644 index 0000000..9faf830 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd @@ -0,0 +1,74 @@ +class_name GdUnitExpressionRunner +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ExpressionRunner extends '${clazz_path}' + +func __run_expression() -> Variant: + return $expression + +""" + +var constructor_args_regex := RegEx.create_from_string("new\\((?.*)\\)") + + +func execute(src_script: GDScript, value: Variant) -> Variant: + if typeof(value) != TYPE_STRING: + return value + + var expression: String = value + var parameter_map := src_script.get_script_constant_map() + for key: String in parameter_map.keys(): + var parameter_value: Variant = parameter_map[key] + # check we need to construct from inner class + # we need to use the original class instance from the script_constant_map otherwise we run into a runtime error + if expression.begins_with(key + ".new") and parameter_value is GDScript: + var object: GDScript = parameter_value + var args := build_constructor_arguments(parameter_map, expression.substr(expression.find("new"))) + if args.is_empty(): + return object.new() + return object.callv("new", args) + + var script := GDScript.new() + var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path + script.source_code = CLASS_TEMPLATE.dedent()\ + .replace("${clazz_path}", resource_path)\ + .replace("$expression", expression) + #script.take_over_path(resource_path) + @warning_ignore("return_value_discarded") + script.reload(true) + var runner: Object = script.new() + if runner.has_method("queue_free"): + (runner as Node).queue_free() + @warning_ignore("unsafe_method_access") + return runner.__run_expression() + + +func build_constructor_arguments(parameter_map: Dictionary, expression: String) -> Array[Variant]: + var result := constructor_args_regex.search(expression) + var extracted_arguments := result.get_string("args").strip_edges() + if extracted_arguments.is_empty(): + return [] + var arguments :Array = extracted_arguments.split(",") + return arguments.map(func(argument: String) -> Variant: + var value := argument.strip_edges() + + # is argument an constant value + if parameter_map.has(value): + return parameter_map[value] + # is typed named value like Vector3.ONE + for type:int in GdObjects.TYPE_AS_STRING_MAPPINGS: + var type_as_string:String = GdObjects.TYPE_AS_STRING_MAPPINGS[type] + if value.begins_with(type_as_string): + return type_convert(value, type) + # is value a string + if value.begins_with("'") or value.begins_with('"'): + return value.trim_prefix("'").trim_suffix("'").trim_prefix('"').trim_suffix('"') + # fallback to default value converting + return str_to_var(value) + ) + + +func to_fuzzer(src_script: GDScript, expression: String) -> Fuzzer: + @warning_ignore("unsafe_cast") + return execute(src_script, expression) as Fuzzer diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid new file mode 100644 index 0000000..ac801aa --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid @@ -0,0 +1 @@ +uid://0ggpxyupbt46 diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd new file mode 100644 index 0000000..527661d --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd @@ -0,0 +1,163 @@ +## @deprecated see GdFunctionParameterSetResolver +class_name GdUnitTestParameterSetResolver +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ParameterExtractor extends '${clazz_path}' + +func __extract_test_parameters() -> Array: + return ${test_params} + +""" + +const EXCLUDE_PROPERTIES_TO_COPY = [ + "script", + "type", + "Node", + "_import_path"] + + +var _fd: GdFunctionDescriptor +var _static_sets_by_index := {} +var _is_static := true + +func _init(fd: GdFunctionDescriptor) -> void: + _fd = fd + + +func is_parameterized() -> bool: + return _fd.is_parameterized() + + +func is_parameter_sets_static() -> bool: + return _is_static + + +func is_parameter_set_static(index: int) -> bool: + return _is_static and _static_sets_by_index.get(index, false) + + +# validates the given arguments are complete and matches to required input fields of the test function +func validate(parameter_sets: Array, parameter_set_index: int) -> GdUnitResult: + if parameter_sets.size() < parameter_set_index: + return GdUnitResult.error("Internal error: the resolved paremeterset has invalid size.") + + var input_values: Array = parameter_sets[parameter_set_index] + if input_values == null: + return GdUnitResult.error("The parameter set '%s' must be an Array!" % parameter_sets[parameter_set_index]) + + # check given parameter set with test case arguments + var input_arguments := _fd.args() + var expected_arg_count := input_arguments.size() - 1 #(-1 we exclude the parameter set itself) + var current_arg_count := input_values.size() + if current_arg_count != expected_arg_count: + var arg_names := input_arguments\ + .filter(func(arg: GdFunctionArgument) -> bool: return not arg.is_parameter_set())\ + .map(func(arg: GdFunctionArgument) -> String: return str(arg)) + + return GdUnitResult.error(""" + The test data set at index (%d) does not match the expected test arguments: + test function: [color=snow]func test...(%s)[/color] + test input values: [color=snow]%s[/color] + """ + .dedent() % [parameter_set_index, ",".join(arg_names), input_values]) + return GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, input_values) + + +static func validate_parameter_types(input_arguments: Array[GdFunctionArgument], input_values: Array) -> GdUnitResult: + for i in input_arguments.size(): + var input_param: GdFunctionArgument = input_arguments[i] + # only check the test input arguments + if input_param.is_parameter_set(): + continue + var input_param_type := input_param.type() + var input_value :Variant = input_values[i] + var input_value_type := typeof(input_value) + # input parameter is not typed or is Variant we skip the type test + if input_param_type == TYPE_NIL or input_param_type == GdObjects.TYPE_VARIANT: + continue + # is input type enum allow int values + if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT: + continue + # allow only equal types and object == null + if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL: + continue + if input_param_type != input_value_type: + return GdUnitResult.error(""" + The test data value does not match the expected input type! + input value: [color=snow]'%s', <%s>[/color] + expected argument: [color=snow]%s[/color] + """ + .dedent() % [input_value, type_string(input_value_type), str(input_param)]) + return GdUnitResult.success("No errors found.") + + +func _extract_property_names(node :Node) -> PackedStringArray: + return node.get_property_list()\ + .map(func(property :Dictionary) -> String: return property["name"])\ + .filter(func(property :String) -> bool: return !EXCLUDE_PROPERTIES_TO_COPY.has(property)) + + +# tests if the test property set contains an property reference by name, if not the parameter set holds only static values +func _is_static_parameter_set(parameters :String, property_names :PackedStringArray) -> bool: + for property_name in property_names: + if parameters.contains(property_name): + _is_static = false + return false + return true + + +# extracts the arguments from the given test case, using kind of reflection solution +# to restore the parameters from a string representation to real instance type +func load_parameter_sets(test_suite: Node) -> GdUnitResult: + var source_script: Script = test_suite.get_script() + var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args()) + var source_code := CLASS_TEMPLATE \ + .replace("${clazz_path}", source_script.resource_path) \ + .replace("${test_params}", parameter_arg.value_as_string()) + var script := GDScript.new() + script.source_code = source_code + # enable this lines only for debuging + #script.resource_path = GdUnitFileAccess.create_temp_dir("parameter_extract") + "/%s__.gd" % test_case.get_name() + #DirAccess.remove_absolute(script.resource_path) + #ResourceSaver.save(script, script.resource_path) + var result := script.reload() + if result != OK: + return GdUnitResult.error("Extracting test parameters failed! Script loading error: %s" % error_string(result)) + var instance :Object = script.new() + GdUnitTestParameterSetResolver.copy_properties(test_suite, instance) + (instance as Node).queue_free() + var parameter_sets: Array = instance.call("__extract_test_parameters") + fixure_typed_parameters(parameter_sets, _fd.args()) + return GdUnitResult.success(parameter_sets) + + +func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array: + for parameter_set_index in parameter_sets.size(): + var parameter_set: Array = parameter_sets[parameter_set_index] + # run over all function arguments + for parameter_index in parameter_set.size(): + var parameter :Variant = parameter_set[parameter_index] + var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index] + if parameter is Array: + var as_array: Array = parameter + # we need to convert the untyped array to the expected typed version + if arg_descriptor.is_typed_array(): + parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null) + return parameter_sets + + +static func copy_properties(source: Object, dest: Object) -> void: + for property in source.get_property_list(): + var property_name :String = property["name"] + var property_value :Variant = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid new file mode 100644 index 0000000..5a3b4cb --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid @@ -0,0 +1 @@ +uid://b7bc2qhbplceo diff --git a/addons/gdUnit4/src/core/report/GdUnitError.gd b/addons/gdUnit4/src/core/report/GdUnitError.gd new file mode 100644 index 0000000..569e013 --- /dev/null +++ b/addons/gdUnit4/src/core/report/GdUnitError.gd @@ -0,0 +1,12 @@ +class_name GdUnitError +extends RefCounted + +var _message: String +var _line_number: int +var _stack_trace: GdUnitStackTrace + + +func _init(message: String, line_number: int, stack_trace: GdUnitStackTrace) -> void: + _message = message + _line_number = line_number + _stack_trace = stack_trace diff --git a/addons/gdUnit4/src/core/report/GdUnitError.gd.uid b/addons/gdUnit4/src/core/report/GdUnitError.gd.uid new file mode 100644 index 0000000..d9cdcf0 --- /dev/null +++ b/addons/gdUnit4/src/core/report/GdUnitError.gd.uid @@ -0,0 +1 @@ +uid://brqr4m6lyabdu diff --git a/addons/gdUnit4/src/core/report/GdUnitReport.gd b/addons/gdUnit4/src/core/report/GdUnitReport.gd new file mode 100644 index 0000000..4e350e2 --- /dev/null +++ b/addons/gdUnit4/src/core/report/GdUnitReport.gd @@ -0,0 +1,107 @@ +class_name GdUnitReport +extends Resource + +# report type +enum { + SUCCESS, + WARN, + FAILURE, + ORPHAN, + TERMINATED, + INTERUPTED, + ABORT, + SKIPPED, +} + +var _type :int +var _line_number :int +var _message :String +var _current_value: Variant +var _error: GdUnitError + + +func from_error(p_type: int, error: GdUnitError) -> GdUnitReport: + _type = p_type + _line_number = error._line_number + _message = error._message + _error = error + return self + + +func create(p_type :int, p_line_number :int, p_message :String) -> GdUnitReport: + _type = p_type + _line_number = p_line_number + _message = p_message + return self + + +func with_current_value(value: Variant) -> GdUnitReport: + _current_value = value + return self + + +func type() -> int: + return _type + + +func line_number() -> int: + return _line_number + + +func message() -> String: + return _message + + +func is_skipped() -> bool: + return _type == SKIPPED + + +func is_warning() -> bool: + return _type == WARN + + +func is_failure() -> bool: + return _type == FAILURE + + +func is_error() -> bool: + return _type == TERMINATED or _type == INTERUPTED or _type == ABORT + + +func is_orphan() -> bool: + return _type == ORPHAN + + +func stack_trace() -> GdUnitStackTrace: + if _error == null: + return null + return _error._stack_trace + + +func _to_string() -> String: + if _line_number == -1: + return "[color=green]line [/color][color=aqua]:[/color] %s" % [_message] + return "[color=green]line [/color][color=aqua]%d:[/color] %s" % [_line_number, _message] + + +func serialize() -> Dictionary: + var serialized := { + "type" : _type, + "line_number" : _line_number, + "message" : _message + } + var trace := stack_trace() + if trace != null: + serialized["stack_trace"] = trace.serialize() + return serialized + + +func deserialize(serialized: Dictionary) -> GdUnitReport: + _type = serialized["type"] + _line_number = serialized["line_number"] + _message = serialized["message"] + if serialized.has("stack_trace"): + @warning_ignore("unsafe_cast") + var trace := GdUnitStackTrace.deserialize(serialized["stack_trace"] as String) + _error = GdUnitError.new(_message, _line_number, trace) + return self diff --git a/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid b/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid new file mode 100644 index 0000000..2c407a0 --- /dev/null +++ b/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid @@ -0,0 +1 @@ +uid://dpmfqonlb3xpq diff --git a/addons/gdUnit4/src/core/runners/GdUnitScriptErrorCollector.gd b/addons/gdUnit4/src/core/runners/GdUnitScriptErrorCollector.gd new file mode 100644 index 0000000..ef2a517 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitScriptErrorCollector.gd @@ -0,0 +1,66 @@ +## Collects GDScript parse errors.[br] +## [br] +## Installed via [method OS.add_logger] so it intercepts [constant ErrorType.ERROR_TYPE_SCRIPT] +## entries without triggering Godot's interactive CLI debugger.[br] +## Used by [class GdUnitTestCIRunner] to detect broken user test scripts early +## and exit with a meaningful error instead of silently skipping them. +class_name GdUnitScriptErrorCollector +extends Logger + + +## Holds information about a single captured script error. +class ScriptError extends RefCounted: + var _message: String + var _source_file: String + var _source_line: int + + func _init(message: String, source_file: String, source_line: int) -> void: + _message = message + _source_file = source_file + _source_line = source_line + + func _to_string() -> String: + if _source_file.is_empty(): + return _message + return "%s\n\tat %s:%d" % [_message, _source_file, _source_line] + + +var _errors: Array[ScriptError] = [] + + +func _init() -> void: + OS.add_logger(self) + + +func _log_error( + _function: String, + source_file: String, + source_line: int, + message: String, + _rationale: String, + _editor_notify: bool, + error_type: int, + _script_backtraces: Array[ScriptBacktrace] + ) -> void: + if error_type != ErrorType.ERROR_TYPE_SCRIPT: + return + _errors.append(ScriptError.new(message, source_file, source_line)) + + +func _log_message(_message: String, _error: bool) -> void: + pass + + +## Returns true if any script errors were captured. +func has_errors() -> bool: + return not _errors.is_empty() + + +## Returns all captured script errors. +func errors() -> Array[ScriptError]: + return _errors + + +## Clears all captured errors. +func clear() -> void: + _errors.clear() diff --git a/addons/gdUnit4/src/core/runners/GdUnitScriptErrorCollector.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitScriptErrorCollector.gd.uid new file mode 100644 index 0000000..2178316 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitScriptErrorCollector.gd.uid @@ -0,0 +1 @@ +uid://18c8wn85tyln diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd new file mode 100644 index 0000000..e680e25 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd @@ -0,0 +1,475 @@ +#warning-ignore-all:return_value_discarded +class_name GdUnitTestCIRunner +extends "res://addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd" +## Command line test runner implementation.[br] +## [br] +## This runner is designed for CI/CD pipelines and command line test execution.[br] +## Features:[br] +## - Command line options for test configuration[br] +## - HTML and JUnit report generation[br] +## - Console output with colored formatting[br] +## - Progress and error reporting[br] +## - Test history management[br] +## [br] +## Example usage:[br] +## [codeblock] +## # Run all tests in a directory +## runtest -a +## +## # Run specific test suite with ignored tests +## runtest -a -i +## [/codeblock] + +var _console := GdUnitCSIMessageWriter.new() +var _console_reporter: GdUnitConsoleTestReporter +var _headless_mode_ignore := false +var _runner_config_file := "" +var _debug_cmd_args := PackedStringArray() +var _included_tests := PackedStringArray() +var _excluded_tests := PackedStringArray() + +## Command line options configuration +var _cmd_options := CmdOptions.new([ + CmdOption.new( + "-a, --add", + "-a ", + "Adds the given test suite or directory to the execution pipeline.", + TYPE_STRING + ), + CmdOption.new( + "-i, --ignore", + "-i ", + "Adds the given test suite or test case to the ignore list.", + TYPE_STRING + ), + CmdOption.new( + "-c, --continue", + "", + """By default GdUnit will abort checked first test failure to be fail fast, + instead of stop after first failure you can use this option to run the complete test set.""".dedent() + ), + CmdOption.new( + "-conf, --config", + "-conf [testconfiguration.cfg]", + "Run all tests by given test configuration. Default is 'GdUnitRunner.cfg'", + TYPE_STRING, + true + ), + CmdOption.new( + "-help", "", + "Shows this help message." + ), + CmdOption.new("--help-advanced", "", + "Shows advanced options." + ) + ], + [ + # advanced options + 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 + ), + CmdOption.new( + "-rc, --report-count", + "-rc ", + "Specifies how many reports are saved before they are deleted. The default is %s." % str(GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT), + TYPE_INT, + true + ), + #CmdOption.new("--list-suites", "--list-suites [directory]", "Lists all test suites located in the given directory.", TYPE_STRING), + #CmdOption.new("--describe-suite", "--describe-suite ", "Shows the description of selected test suite.", TYPE_STRING), + CmdOption.new( + "--info", "", + "Shows the GdUnit version info" + ), + CmdOption.new( + "--selftest", "", + "Runs the GdUnit self test" + ), + CmdOption.new( + "--ignoreHeadlessMode", + "--ignoreHeadlessMode", + "By default, running GdUnit4 in headless mode is not allowed. You can switch off the headless mode check by set this property." + ), + ]) + + +func _init() -> void: + super() + + +func _ready() -> void: + super() + # stop checked first test failure to fail fast + _executor.fail_fast(true) + _console_reporter = GdUnitConsoleTestReporter.new(_console, true) + GdUnitSignals.instance().gdunit_message.connect(_on_send_message) + + +func _notification(what: int) -> void: + super(what) + + +func init_runner() -> void: + await init_gd_unit() + + +## Returns the exit code based on test results.[br] +## Maps test report status to process exit codes. +func get_exit_code() -> int: + return report_exit_code() + + +## Cleanup and quit the runner.[br] +## [br] +## [param code] The exit code to return. +func quit(code: int) -> void: + await super(code) + get_tree().quit(code) + + +## Prints info message to console.[br] +## [br] +## [param message] The message to print.[br] +## [param color] Optional color for the message. +func console_info(message: String, color: Color = Color.WHITE) -> void: + _console.color(color).println_message(message) + + +## Prints error message to console.[br] +## [br] +## [param message] The error message to print. +func console_error(message: String) -> void: + _console.prints_error(message) + + +## Prints warning message to console.[br] +## [br] +## [param message] The warning message to print. +func console_warning(message: String) -> void: + _console.prints_warning(message) + + +## Sets the directory for test reports.[br] +## [br] +## [param path] The path where reports should be written. +func set_report_dir(path: String) -> void: + report_base_path = ProjectSettings.globalize_path(GdUnitFileAccess.make_qualified_path(path)) + console_info( + "Set write reports to %s" % report_base_path, + Color.DEEP_SKY_BLUE + ) + + +## Sets how many report files to keep.[br] +## [br] +## [param count] The number of reports to keep. +func set_report_count(count: String) -> void: + var report_count := count.to_int() + if report_count < 1: + console_error( + "Invalid report history count '%s' set back to default %d" + % [count, GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT] + ) + max_report_history = GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT + else: + console_info( + "Set report history count to %s" % count, + Color.DEEP_SKY_BLUE + ) + max_report_history = report_count + + +## Disables fail-fast mode to run all tests.[br] +## By default tests stop on first failure. +func disable_fail_fast() -> void: + console_info( + "Disabled fail fast!", + Color.DEEP_SKY_BLUE + ) + @warning_ignore("unsafe_method_access") + _executor.fail_fast(false) + + +func run_self_test() -> void: + console_info( + "Run GdUnit4 self tests.", + Color.DEEP_SKY_BLUE + ) + disable_fail_fast() + + + +## Shows GdUnit and Godot version information. +func show_version() -> void: + console_info( + "Godot %s" % Engine.get_version_info().get("string") as String, + Color.DARK_SALMON + ) + var config := ConfigFile.new() + config.load("addons/gdUnit4/plugin.cfg") + console_info( + "GdUnit4 %s" % config.get_value("plugin", "version") as String, + Color.DARK_SALMON + ) + quit(RETURN_SUCCESS) + + +## Ignores headless mode restrictions.[br] +## Allows tests to run in headless mode despite limitations. +func check_headless_mode() -> void: + _headless_mode_ignore = true + + +## Shows available command line options.[br] +## [br] +## [param show_advanced] Whether to show advanced options. +func show_options(show_advanced: bool = false) -> void: + console_info( + """ + Usage: + runtest -a + runtest -a -i + """.dedent(), + Color.DARK_SALMON + ) + console_info( + "-- Options ---------------------------------------------------------------------------------------", + Color.DARK_SALMON + ) + for option in _cmd_options.default_options(): + describe_option(option) + if show_advanced: + console_info( + "-- Advanced options --------------------------------------------------------------------------", + Color.DARK_SALMON + ) + for option in _cmd_options.advanced_options(): + describe_option(option) + + +## Describes a single command line option.[br] +## [br] +## [param cmd_option] The option to describe. +func describe_option(cmd_option: CmdOption) -> void: + console_info( + " %-40s" % str(cmd_option.commands()), + Color.CORNFLOWER_BLUE + ) + console_info( + cmd_option.description(), + Color.LIGHT_GREEN + ) + if not cmd_option.help().is_empty(): + console_info( + "%-4s %s" % ["", cmd_option.help()], + Color.DARK_TURQUOISE + ) + console_info("") + + +## Loads test configuration from file.[br] +## [br] +## [param path] Path to the configuration file. +func load_test_config(path := GdUnitRunnerConfig.CONFIG_FILE) -> void: + console_info( + "Loading test configuration %s\n" % path, + Color.CORNFLOWER_BLUE + ) + _runner_config_file = path + _runner_config.load_config(path) + + +## Shows basic help and exits. +func show_help() -> void: + show_options() + quit(RETURN_SUCCESS) + + +## Shows advanced help and exits. +func show_advanced_help() -> void: + show_options(true) + quit(RETURN_SUCCESS) + + +## Gets command line arguments.[br] +## Returns debug args if set, otherwise actual command line args. +func get_cmdline_args() -> PackedStringArray: + if _debug_cmd_args.is_empty(): + return OS.get_cmdline_args() + return _debug_cmd_args + + +## Initializes the test runner and processes command line arguments. +func init_gd_unit() -> void: + console_info( + """ + -------------------------------------------------------------------------------------------------- + GdUnit4 Comandline Tool + --------------------------------------------------------------------------------------------------""".dedent(), + Color.DARK_SALMON + ) + + var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") + var result := cmd_parser.parse(get_cmdline_args()) + if result.is_error(): + console_error(result.error_message()) + show_options() + console_error("Abnormal exit with %d" % RETURN_ERROR) + quit(RETURN_ERROR) + return + if result.is_empty(): + show_help() + return + # build runner config by given commands + var commands :Array[CmdCommand] = [] + @warning_ignore("unsafe_cast") + commands.append_array(result.value() as Array) + result = ( + CmdCommandHandler.new(_cmd_options) + .register_cb("-help", show_help) + .register_cb("--help-advanced", show_advanced_help) + .register_cb("-a", add_test_suite) + .register_cbv("-a", add_test_suites) + .register_cb("-i", skip_test_suite) + .register_cbv("-i", skip_test_suites) + .register_cb("-rd", set_report_dir) + .register_cb("-rc", set_report_count) + .register_cb("--selftest", run_self_test) + .register_cb("-c", disable_fail_fast) + .register_cb("-conf", load_test_config) + .register_cb("--info", show_version) + .register_cb("--ignoreHeadlessMode", check_headless_mode) + .execute(commands) + ) + if result.is_error(): + console_error(result.error_message()) + quit(RETURN_ERROR) + return + + if DisplayServer.get_name() == "headless": + if _headless_mode_ignore: + console_warning(""" + Headless mode is ignored by option '--ignoreHeadlessMode'" + + Please note that tests that use UI interaction do not work correctly in headless mode. + Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore + have no effect in the test! + """.dedent() + ) + else: + console_error(""" + Headless mode is not supported! + + Please note that tests that use UI interaction do not work correctly in headless mode. + Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore + have no effect in the test! + + You can run with '--ignoreHeadlessMode' to swtich off this check. + """.dedent() + ) + console_error( + "Abnormal exit with %d" % RETURN_ERROR_HEADLESS_NOT_SUPPORTED + ) + quit(RETURN_ERROR_HEADLESS_NOT_SUPPORTED) + return + + var script_error_collector := GdUnitScriptErrorCollector.new() + _test_cases = discover_tests() + + # Check for script errors captured during discovery + if script_error_collector.has_errors(): + console_error("Script errors were detected during test discovery!") + for error in script_error_collector.errors(): + console_info(" %s" % error) + console_error("Abnormal exit with %d" % RETURN_ERROR_SCRIPT_ERRORS_DETECTED) + await quit(RETURN_ERROR_SCRIPT_ERRORS_DETECTED) + return + + if _test_cases.is_empty(): + console_info("No test cases found, abort test run!", Color.YELLOW) + console_info("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) + await quit(RETURN_SUCCESS) + return + _state = RUN + + +func discover_tests() -> Array[GdUnitTestCase]: + var gdunit_test_discover_added := GdUnitSignals.instance().gdunit_test_discover_added + + _test_cases = _runner_config.test_cases() + var scanner := GdUnitTestSuiteScanner.new() + for path in _included_tests: + var scripts := scanner.scan(path) + for script in scripts: + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + if not is_skipped(test): + #_console.println_message("discoverd %s" % test.display_name) + _test_cases.append(test) + gdunit_test_discover_added.emit(test) + ) + + return _test_cases + + +func add_test_suite(path: String) -> void: + _included_tests.append(path) + + +func add_test_suites(paths: PackedStringArray) -> void: + _included_tests.append_array(paths) + + +func skip_test_suite(path: String) -> void: + _excluded_tests.append(path) + + +func skip_test_suites(paths: PackedStringArray) -> void: + _excluded_tests.append_array(paths) + + +func is_skipped(test: GdUnitTestCase) -> bool: + for skipped_info in _excluded_tests: + + # is suite skipped by full path or suite name + if skipped_info == test.suite_name or test.source_file.contains(skipped_info): + return true + var skip_file := skipped_info.replace("res://", "") + + # check for skipped single test + if not skip_file.contains(":"): + continue + var parts: PackedStringArray = skip_file.rsplit(":") + var skipped_suite := parts[0] + var skipped_test := parts[1] + # is suite skipped by full path or suite name + if (skipped_suite == test.suite_name or test.source_file.contains(skipped_suite)) and skipped_test == test.test_name: + return true + + return false + + +func _on_send_message(message: String) -> void: + _console.color(Color.CORNFLOWER_BLUE).println_message(message) + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.SESSION_START: + _console_reporter.test_session = _test_session + GdUnitEvent.SESSION_CLOSE: + _console_reporter.test_session = null + + +func report_exit_code() -> int: + if _console_reporter.total_error_count() + _console_reporter.total_failure_count() > 0: + console_info("Exit code: %d" % RETURN_ERROR, Color.FIREBRICK) + return RETURN_ERROR + if _console_reporter.total_orphan_count() > 0: + console_info("Exit code: %d" % RETURN_WARNING, Color.GOLDENROD) + return RETURN_WARNING + console_info("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) + return RETURN_SUCCESS diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid new file mode 100644 index 0000000..cc02280 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid @@ -0,0 +1 @@ +uid://bxg6i2k555h48 diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd new file mode 100644 index 0000000..9132878 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd @@ -0,0 +1,79 @@ +extends "res://addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd" +## Runner implementation used by the editor UI.[br] +## [br] +## This runner connects to a GdUnit server via TCP to report test results.[br] +## Test results are reported in real-time and displayed in the editor UI.[br] +## [br] +## The runner uses an RPC message protocol to communicate status and events:[br] +## - Messages to report progress[br] +## - Events to report test results[br] + +## The TCP client used to connect to the GdUnit server +@onready var _client: GdUnitTcpClient = $GdUnitTcpClient +@onready var _version_label: Control = %Version + + +func _init() -> void: + super() + # We set the default max report history to 1 + max_report_history = 1 + + +func _ready() -> void: + super() + GdUnit4Version.init_version_label(_version_label) + + var config_result := _runner_config.load_config() + if config_result.is_error(): + push_error(config_result.error_message()) + _state = EXIT + return + @warning_ignore("return_value_discarded") + _client.connection_failed.connect(_on_connection_failed) + GdUnitSignals.instance().gdunit_message.connect(_on_send_message) + _executor.fail_fast(_runner_config.is_fail_fast()) + var result := _client.start("127.0.0.1", _runner_config.server_port()) + if result.is_error(): + push_error(result.error_message()) + return + + +## Called when the TCP connection to the GdUnit server fails.[br] +## Stops the test execution.[br] +## [br] +## [param message] The error message describing the failure. +func _on_connection_failed(message: String) -> void: + prints("_on_connection_failed", message) + _state = STOP + + +## Initializes the test runner.[br] +## Waits for TCP client connection and then scans for test suites.[br] +## Reports the number of found test suites via TCP message. +func init_runner() -> void: + # wait until client is connected to the GdUnitServer + if _client.is_client_connected(): + await gdUnitInit() + _state = RUN + + +## Initializes the GdUnit framework.[br] +## Sends initial message about number of test suites. +func gdUnitInit() -> void: + #enable_manuall_polling() + _test_cases = _runner_config.test_cases() + await get_tree().process_frame + + +## Sends a message via TCP to the GdUnit server.[br] +## [br] +## [param message] The message to send. +func _on_send_message(message: String) -> void: + _client.send(RPCMessage.of(message)) + + +## Handles GdUnit events by sending them via TCP to the server.[br] +## [br] +## [param event] The event to send. +func _on_gdunit_event(event: GdUnitEvent) -> void: + _client.send(RPCGdUnitEvent.of(event)) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid new file mode 100644 index 0000000..8559bae --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid @@ -0,0 +1 @@ +uid://cey3b0jvfyrf diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn new file mode 100644 index 0000000..eaa6f1a --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"] + +[node name="Control" type="Node"] +script = ExtResource("1") + +[node name="GdUnitTcpClient" type="Node" parent="."] +script = ExtResource("2") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +custom_minimum_size = Vector2(0, 24) +layout_direction = 2 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 0 +size_flags_horizontal = 3 +size_flags_vertical = 10 +alignment = 2 + +[node name="Version" type="RichTextLabel" parent="HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(128, 0) +layout_mode = 2 +size_flags_horizontal = 10 +bbcode_enabled = true +scroll_active = false +shortcut_keys_enabled = false +horizontal_alignment = 1 +justification_flags = 0 diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd new file mode 100644 index 0000000..c789847 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd @@ -0,0 +1,169 @@ +## +## @since GdUnit4 5.1.0 +## +## Represents a test execution session in GdUnit4.[br] +## [br] +## [i]A test session encapsulates a complete test execution cycle, managing the collection +## of test cases to be executed and providing communication channels for test events +## and messages. This class serves as the central coordination point for test execution +## and allows hooks and other components to interact with the running test session.[/i][br] +## [br] +## [b][u]Key Features[/u][/b][br] +## - [i][b]Test Case Management[/b][/i]: Maintains a collection of test cases to be executed[br] +## - [i][b]Event Broadcasting[/b][/i]: Forwards GdUnit events to session-specific listeners[br] +## - [i][b]Message Communication[/b][/i]: Provides a channel for sending messages during test execution[br] +## - [i][b]Hook Integration[/b][/i]: Passed to test session hooks for startup and shutdown operations[br] +## [br] +## [b][u]Usage in Test Hooks[/u][/b] +## [codeblock] +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## # Access test cases +## print("Running %d test cases" % session.test_cases.size()) +## +## # Send status messages +## session.send_message("Custom hook initialized") +## +## # Listen for test events +## session.test_event.connect(_on_test_event) +## +## return GdUnitResult.success() +## +## func _on_test_event(event: GdUnitEvent) -> void: +## print("Test event received: %s" % event.type) +## [/codeblock] +## [br] +## [b][u]Event Flow[/u][/b][br] +## 1. Session is created with a collection of test cases[br] +## 2. Session connects to the global GdUnit event system[br] +## 3. During test execution, events are automatically forwarded to session listeners[br] +## 4. Hooks and other components can subscribe to session events[br] +## 5. Messages can be sent through the session for logging and communication[br] +class_name GdUnitTestSession +extends RefCounted + + +## Emitted when a test execution event occurs.[br] +## [br] +## [i]This signal forwards events from the global GdUnit event system to session-specific +## listeners. It allows hooks and other session components to react to test events +## without directly connecting to the global event system.[/i][br] +## [br] +## [u]Common event types include:[/u][br] +## - Test suite start/end events[br] +## - Test case start/end events[br] +## - Test assertion events[br] +## - Test failure/error events[br] +## +## [param event] The test event containing details about test execution, timing, and results +@warning_ignore("unused_signal") +signal test_event(event: GdUnitEvent) + + +## [b][color=red]@readonly: Should not be modified directly during test execution![/color][/b][br] +## Collection of test cases to be executed in this session.[br] +## [br] +## This array contains all the test cases that will be run during the session. +## Test hooks can access this collection to: +## - Get the total number of tests to be executed +## - Access individual test case metadata +## - Perform setup/teardown based on test case requirements +## - Generate reports or statistics about the test suite +## +## The collection is typically populated before session startup and remains +## constant during test execution. +var _test_cases : Array[GdUnitTestCase] = [] + + +## [b][color=red]@readonly: The report path should not be modified after session creation![/color][/b][br] +## The file system path where test reports for this session will be generated.[br] +## [br] +## [i]This property provides centralized access to the report output location, +## allowing test hooks, reporters, and other components to reference the same +## report path without coupling to specific reporter implementations.[/i][br] +## [br] +## [b][u]Common use cases include:[/u][/b][br] +## - Test hooks generating additional report files in the same directory[br] +## - Custom reporters creating supplementary output files[br] +## - Post-processing scripts that need to locate generated reports[br] +## - Cleanup operations that need to manage report artifacts[br] +## [br] +## [b][u]Example Usage:[/u][/b] +## [codeblock] +## # In a test hook +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## var report_dir = session.report_path.get_base_dir() +## var custom_report = report_dir.path_join("custom_metrics.json") +## # Generate additional reports in the same location +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Reports available at: " + session.report_path) +## return GdUnitResult.success() +## [/codeblock] +## [br] +## The path is set during session initialization and remains constant throughout +## the test execution lifecycle. +var report_path: String: + get: + return report_path + + +## Initializes the test session and sets up event forwarding.[br] +## [br] +## [i]This constructor automatically connects to the global GdUnit event system +## and forwards all events to the session's test_event signal. This allows +## session-specific components to listen for test events without managing +## global signal connections.[/i] +func _init(test_cases: Array[GdUnitTestCase], session_report_path: String) -> void: + # We build a copy to prevent a user is modifing the tests + _test_cases = test_cases.duplicate(true) + report_path = session_report_path + GdUnitSignals.instance().gdunit_event.connect(func(event: GdUnitEvent) -> void: + test_event.emit(event) + ) + + +## Finds a test case by its unique identifier.[br] +## [br] +## [i]Searches through all test cases to find a test with the matching GUID.[/i][br] +## [br] +## [param id] The GUID of the test to find[br] +## Returns the matching test case or null if not found. +func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase: + for test in _test_cases: + if test.guid.equals(id): + return test + + return null + + +## Sends a message through the GdUnit messaging system.[br] +## [br] +## [i]This method provides a convenient way for test hooks and other session +## components to send messages that will be handled by the GdUnit framework.[/i] +## [br][br] +## [b][u]Messages are typically used for:[/u][/b][br] +## - Status updates during test execution[br] +## - Progress reporting from test hooks[br] +## - Debug information and logging[br] +## - User notifications and alerts[br] +## [br] +## The message will be processed by the global GdUnit message system and +## may be displayed in the test runner UI, logged to files, or handled +## by other registered message handlers. +## [br] +## [b][u]Example Usage:[/u][/b] +## [codeblock] +## # In a test hook +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Database connection established") +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Generated test report: report.html") +## return GdUnitResult.success() +## ``` +## [/codeblock] +## [param message] The message text to send through the GdUnit messaging system +func send_message(message: String) -> void: + GdUnitSignals.instance().gdunit_message.emit(message) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid new file mode 100644 index 0000000..dafd1f8 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid @@ -0,0 +1 @@ +uid://gofenlm417m0 diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd new file mode 100644 index 0000000..708ed4d --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd @@ -0,0 +1,192 @@ +extends Node +## The base test runner implementation.[br] +## [br] +## This class provides the core functionality to execute test suites with following features:[br] +## - Loading and initialization of test suites[br] +## - Executing test suites and managing test states[br] +## - Event dispatching and test reporting[br] +## - Support for headless mode[br] +## - Plugin version verification[br] +## [br] +## Supported by specialized runners:[br] +## - [b]GdUnitTestRunner[/b]: Used in the editor, connects via tcp to report test results[br] +## - [b]GdUnitCLRunner[/b]: A command line interface runner, writes test reports to file[br] +## The test runner runs checked default in fail-fast mode, it stops checked first test failure. + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +## Overall test run status codes used by the runners +const RETURN_SUCCESS = 0 +const RETURN_ERROR = 100 +const RETURN_ERROR_HEADLESS_NOT_SUPPORTED = 103 +const RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED = 104 +const RETURN_ERROR_SCRIPT_ERRORS_DETECTED = 105 +const RETURN_WARNING = 101 + +## Specifies the Node name under which the runner is registered +const GDUNIT_RUNNER = "GdUnitRunner" + +## The current runner configuration +@warning_ignore("unused_private_class_variable") +var _runner_config := GdUnitRunnerConfig.new() + +## The test suite executor instance +var _executor: GdUnitTestSuiteExecutor +var _hooks : GdUnitTestSessionHookService + +## Current runner state +var _state := READY + +## Current tests to be processed +var _test_cases: Array[GdUnitTestCase] = [] + + +## Configured report base path (can be set on CI test runner) +var report_base_path: String = GdUnitFileAccess.current_dir() + "reports": + get: + return report_base_path + + +## Current session report path +var report_path: String: + get: + return "%s/%s%d" % [report_base_path, GdUnitConstants.REPORT_DIR_PREFIX, current_report_history_index] + + +## Current report history index, if max_report_history > 1 we scan for the next index over the existing reports +var current_report_history_index: int: + get: + if max_report_history > 1: + return GdUnitFileAccess.find_last_path_index(report_base_path, GdUnitConstants.REPORT_DIR_PREFIX) + 1 + else: + return 1 + + +## Controls how many report historys will be hold +var max_report_history: int = GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT: + get: + return max_report_history + set(value): + max_report_history = value + + +# holds the current test session context +var _test_session: GdUnitTestSession +var _is_editor_debug_run: bool + +## Runner state machine +enum { + READY, + INIT, + RUN, + STOP, + EXIT +} + +func _init() -> void: + _is_editor_debug_run = OS.get_cmdline_args().has("--scene") + if _is_editor_debug_run: + DisplayServer.window_set_title("GdUnit4 Runner (Debug Mode)") + else: + DisplayServer.window_set_title("GdUnit4 Runner (Release Mode)") + if not Engine.is_embedded_in_editor(): + # minimize scene window checked debug mode + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + # store current runner instance to engine meta data to can be access in as a singleton + Engine.set_meta(GDUNIT_RUNNER, self) + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + if Engine.get_version_info().hex < 0x40300: + printerr("The GdUnit4 plugin requires Godot version 4.3 or higher to run.") + quit(RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED) + return + _executor = GdUnitTestSuiteExecutor.new() + + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + _state = INIT + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + Engine.remove_meta(GDUNIT_RUNNER) + + +## Main test runner loop. Is called every frame to manage the test execution. +func _process(_delta: float) -> void: + match _state: + INIT: + await init_runner() + RUN: + _hooks = GdUnitTestSessionHookService.instance() + _test_session = GdUnitTestSession.new(_test_cases, report_path) + GdUnitSignals.instance().gdunit_event.emit(GdUnitSessionStart.new()) + # process next test suite + set_process(false) + var result := await _hooks.execute_startup(_test_session) + if result.is_error(): + push_error(result.error_message()) + else: + await _executor.run_and_wait(_test_cases) + result = await _hooks.execute_shutdown(_test_session) + if result.is_error(): + push_error(result.error_message()) + _state = STOP + set_process(true) + GdUnitSignals.instance().gdunit_event.emit(GdUnitSessionClose.new()) + cleanup_report_history() + STOP: + _state = EXIT + # give the engine small amount time to finish the rpc + await get_tree().create_timer(0.1).timeout + await quit(get_exit_code()) + + +## Used by the inheriting runners to initialize test execution +func init_runner() -> void: + await get_tree().process_frame + + +func cleanup_report_history() -> int: + return GdUnitFileAccess.delete_path_index_lower_equals_than( + report_path.get_base_dir(), + GdUnitConstants.REPORT_DIR_PREFIX, + current_report_history_index-1-max_report_history) + + +## Returns the exit code when the test run is finished.[br] +## Abstract method to be implemented by the inheriting runners. +func get_exit_code() -> int: + return RETURN_SUCCESS + + +## Quits the test runner with given exit code. +func quit(_code: int) -> void: + await GdUnitMemoryObserver.gc_on_guarded_instances() + + if !_is_editor_debug_run: + # Only dispose all resources when we not run embedded in the editor + GdUnitTools.dispose_all() + await get_tree().process_frame + + await get_tree().process_frame + await get_tree().physics_frame + + +func prints_warning(message: String) -> void: + prints(message) + + +## Default event handler to process test events.[br] +## Should be overridden by concrete runner implementation. +@warning_ignore("unused_parameter") +func _on_gdunit_event(event: GdUnitEvent) -> void: + pass + + +## Event bridge from C# GdUnit4.ITestEventListener.cs[br] +## Used to handle test events from C# tests. +# gdlint: disable=function-name +func PublishEvent(data: Dictionary) -> void: + _on_gdunit_event(GdUnitEvent.new().deserialize(data)) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid new file mode 100644 index 0000000..7dd0c28 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid @@ -0,0 +1 @@ +uid://b70attg5m7iur diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd new file mode 100644 index 0000000..20325ac --- /dev/null +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd @@ -0,0 +1,36 @@ +class_name GdUnitTestSuiteDefaultTemplate +extends RefCounted + + +const DEFAULT_TEMP_TS_GD =""" + # GdUnit generated TestSuite + class_name ${suite_class_name} + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + # TestSuite generated from + const __source: String = '${source_resource_path}' +""" + + +const DEFAULT_TEMP_TS_CS = """ + // GdUnit generated TestSuite + + using Godot; + using GdUnit4; + + namespace ${name_space} + { + using static Assertions; + using static Utils; + + [TestSuite] + public class ${suite_class_name} + { + // TestSuite generated from + private const string sourceClazzPath = "${source_resource_path}"; + + } + } +""" diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid new file mode 100644 index 0000000..2ad010a --- /dev/null +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid @@ -0,0 +1 @@ +uid://dv2sby1leq5px diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd new file mode 100644 index 0000000..6fc282d --- /dev/null +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd @@ -0,0 +1,144 @@ +class_name GdUnitTestSuiteTemplate +extends RefCounted + +const TEMPLATE_ID_GD = 1000 +const TEMPLATE_ID_CS = 2000 + +const SUPPORTED_TAGS_GD = """ + GdScript Tags are replaced when the test-suite is created. + + # The class name of the test-suite, formed from the source script. + ${suite_class_name} + # is used to build the test suite class name + class_name ${suite_class_name} + extends GdUnitTestSuite + + + # The class name in pascal case, formed from the source script. + ${source_class} + # can be used to create the class e.g. for source 'MyClass' + var my_test_class := ${source_class}.new() + # will be result in + var my_test_class := MyClass.new() + + # The class as variable name in snake case, formed from the source script. + ${source_var} + # Can be used to build the variable name e.g. for source 'MyClass' + var ${source_var} := ${source_class}.new() + # will be result in + var my_class := MyClass.new() + + # The full resource path from which the file was created. + ${source_resource_path} + # Can be used to load the script in your test + var my_script := load(${source_resource_path}) + # will be result in + var my_script := load("res://folder/my_class.gd") +""" + +const SUPPORTED_TAGS_CS = """ + C# Tags are replaced when the test-suite is created. + + // The namespace name of the test-suite + ${name_space} + namespace ${name_space} + + // The class name of the test-suite, formed from the source class. + ${suite_class_name} + // is used to build the test suite class name + [TestSuite] + public class ${suite_class_name} + + // The class name formed from the source class. + ${source_class} + // can be used to create the class e.g. for source 'MyClass' + private string myTestClass = new ${source_class}(); + // will be result in + private string myTestClass = new MyClass(); + + // The class as variable name in camelCase, formed from the source class. + ${source_var} + // Can be used to build the variable name e.g. for source 'MyClass' + private object ${source_var} = new ${source_class}(); + // will be result in + private object myClass = new MyClass(); + + // The full resource path from which the file was created. + ${source_resource_path} + // Can be used to load the script in your test + private object myScript = GD.Load(${source_resource_path}); + // will be result in + private object myScript = GD.Load("res://folder/MyClass.cs"); +""" + +const TAG_TEST_SUITE_CLASS = "${suite_class_name}" +const TAG_SOURCE_CLASS_NAME = "${source_class}" +const TAG_SOURCE_CLASS_VARNAME = "${source_var}" +const TAG_SOURCE_RESOURCE_PATH = "${source_resource_path}" + + +static func default_GD_template() -> String: + return GdUnitTestSuiteDefaultTemplate.DEFAULT_TEMP_TS_GD.dedent().trim_prefix("\n") + + +static func default_CS_template() -> String: + return GdUnitTestSuiteDefaultTemplate.DEFAULT_TEMP_TS_CS.dedent().trim_prefix("\n") + + +static func build_template(source_path: String) -> String: + var clazz_name :String = GdObjects.to_pascal_case(GdObjects.extract_class_name(source_path).value_as_string()) + var template: String = GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template()) + + return template\ + .replace(TAG_TEST_SUITE_CLASS, clazz_name+"Test")\ + .replace(TAG_SOURCE_RESOURCE_PATH, source_path)\ + .replace(TAG_SOURCE_CLASS_NAME, clazz_name)\ + .replace(TAG_SOURCE_CLASS_VARNAME, GdObjects.to_snake_case(clazz_name)) + + +static func default_template(template_id :int) -> String: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return "" + if template_id == TEMPLATE_ID_GD: + return default_GD_template() + return default_CS_template() + + +static func load_template(template_id :int) -> String: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return "" + if template_id == TEMPLATE_ID_GD: + return GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template()) + return GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_CS, default_CS_template()) + + +static func save_template(template_id :int, template :String) -> void: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return + if template_id == TEMPLATE_ID_GD: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_GD, template.dedent().trim_prefix("\n")) + elif template_id == TEMPLATE_ID_CS: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_CS, template.dedent().trim_prefix("\n")) + + +static func reset_to_default(template_id :int) -> void: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return + if template_id == TEMPLATE_ID_GD: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template()) + else: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_CS, default_CS_template()) + + +static func load_tags(template_id :int) -> String: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return "Error checked loading tags" + if template_id == TEMPLATE_ID_GD: + return SUPPORTED_TAGS_GD + else: + return SUPPORTED_TAGS_CS diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid new file mode 100644 index 0000000..d9b4e62 --- /dev/null +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid @@ -0,0 +1 @@ +uid://daqq6scat73nj diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd new file mode 100644 index 0000000..89bc48b --- /dev/null +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd @@ -0,0 +1,71 @@ +class_name GdUnitThreadContext +extends RefCounted + +var _thread: Thread +var _thread_name: String +var _thread_id: int +var _signal_collector: GdUnitSignalCollector +var _execution_context: GdUnitExecutionContext +var _asserts := [] + + +func _init(thread: Thread = null) -> void: + if thread != null: + _thread = thread + _thread_name = thread.get_meta("name") + _thread_id = thread.get_id() as int + else: + _thread_name = "main" + _thread_id = OS.get_main_thread_id() + _signal_collector = GdUnitSignalCollector.new() + + +func dispose() -> void: + clear_assert() + if is_instance_valid(_signal_collector): + _signal_collector.clear() + _signal_collector = null + _execution_context = null + _thread = null + + +func terminate() -> void: + if _execution_context: + _execution_context.terminate() + + +func clear_assert() -> void: + _asserts.clear() + + +func set_assert(value: GdUnitAssert) -> void: + if value != null: + _asserts.append(value) + + +func get_assert() -> GdUnitAssert: + return null if _asserts.is_empty() else _asserts[-1] + + +func set_execution_context(context: GdUnitExecutionContext) -> void: + _execution_context = context + + +func get_execution_context() -> GdUnitExecutionContext: + return _execution_context + + +func get_execution_context_id() -> int: + return _execution_context.get_instance_id() + + +func get_signal_collector() -> GdUnitSignalCollector: + return _signal_collector + + +func thread_id() -> int: + return _thread_id + + +func _to_string() -> String: + return "ThreadContext <%s>: %s " % [_thread_name, _thread_id] diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid new file mode 100644 index 0000000..33b7cf6 --- /dev/null +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid @@ -0,0 +1 @@ +uid://c4yr2abkvr56b diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd new file mode 100644 index 0000000..2e694e2 --- /dev/null +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd @@ -0,0 +1,69 @@ +## A manager to run new thread and crate a ThreadContext shared over the actual test run +class_name GdUnitThreadManager +extends Object + +## { = } +var _thread_context_by_id: Dictionary[int, GdUnitThreadContext] = {} +## holds the current thread id +var _current_thread_id :int = -1 + +func _init() -> void: + # add initail the main thread + _current_thread_id = OS.get_thread_caller_id() + _thread_context_by_id[OS.get_main_thread_id()] = GdUnitThreadContext.new() + + +static func instance() -> GdUnitThreadManager: + return GdUnitSingleton.instance("GdUnitThreadManager", func() -> GdUnitThreadManager: return GdUnitThreadManager.new()) + + +## Runs a new thread by given name and Callable.[br] +## A new GdUnitThreadContext is created, which is used for the actual test execution.[br] +## We need this custom implementation while this bug is not solved +## Godot issue https://github.com/godotengine/godot/issues/79637 +static func run(name :String, cb :Callable) -> Variant: + return await instance()._run(name, cb) + + +static func interrupt() -> void: + for thread_context: GdUnitThreadContext in instance()._thread_context_by_id.values(): + thread_context.terminate() + + +## Returns the current valid thread context +static func get_current_context() -> GdUnitThreadContext: + return instance()._get_current_context() + + +func _run(name :String, cb :Callable) -> Variant: + # we do this hack because of `OS.get_thread_caller_id()` not returns the current id + # when await process_frame is called inside the fread + var save_current_thread_id := _current_thread_id + var thread := Thread.new() + thread.set_meta("name", name) + @warning_ignore("return_value_discarded") + thread.start(cb) + _current_thread_id = thread.get_id() as int + _register_thread(thread, _current_thread_id) + var result :Variant = await thread.wait_to_finish() + _unregister_thread(_current_thread_id) + # restore original thread id + _current_thread_id = save_current_thread_id + return result + + +func _register_thread(thread :Thread, thread_id :int) -> void: + var context := GdUnitThreadContext.new(thread) + _thread_context_by_id[thread_id] = context + + +func _unregister_thread(thread_id :int) -> void: + var context: GdUnitThreadContext = _thread_context_by_id.get(thread_id) + if context: + @warning_ignore("return_value_discarded") + _thread_context_by_id.erase(thread_id) + context.dispose() + + +func _get_current_context() -> GdUnitThreadContext: + return _thread_context_by_id.get(_current_thread_id) diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid new file mode 100644 index 0000000..fcd48c2 --- /dev/null +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid @@ -0,0 +1 @@ +uid://gwvsvhdii02r diff --git a/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd new file mode 100644 index 0000000..c7eb9f5 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd @@ -0,0 +1,235 @@ +@tool +class_name GdUnitCSIMessageWriter +extends GdUnitMessageWriter +## A message writer implementation using ANSI/CSI escape codes for console output.[br] +## [br] +## This writer provides formatted message output using CSI (Control Sequence Introducer) codes.[br] +## It supports:[br] +## - Color using RGB values[br] +## - Text styles (bold, italic, underline)[br] +## - Cursor positioning and text alignment[br] +## [br] +## Used primarily for console-based test execution and CI/CD environments. + + +enum { + COLOR_TABLE, + COLOR_RGB +} + +const CSI_BOLD = "" +const CSI_ITALIC = "" +const CSI_UNDERLINE = "" +const CSI_RESET = "" + +# Control Sequence Introducer +var _debug_show_color_codes := false +var _color_mode := COLOR_TABLE + +## Current cursor position in the line +var _current_pos := 0 + +# Pre-compiled regex patterns for tag matching +var _tag_regex: RegEx + + +## Constructs CSI style codes based on flags.[br] +## [br] +## [param flags] The style flags to apply (BOLD, ITALIC, UNDERLINE).[br] +## Returns the corresponding CSI codes. +func _apply_style_flags(flags: int) -> String: + var _style := "" + if flags & BOLD: + _style += CSI_BOLD + if flags & ITALIC: + _style += CSI_ITALIC + if flags & UNDERLINE: + _style += CSI_UNDERLINE + return _style + + +## Converts a color string (named or hex) to a Color object +func _parse_color(color_str: String) -> Color: + return Color.from_string(color_str.strip_edges().to_lower(), Color.WHITE) + + +## Generates CSI color code for foreground color +func _color_to_csi_fg(c: Color) -> String: + return "[38;2;%d;%d;%dm" % [c.r8 * c.a, c.g8 * c.a, c.b8 * c.a] + + +## Generates CSI color code for background color +func _color_to_csi_bg(c: Color) -> String: + return "[48;2;%d;%d;%dm" % [c.r8 * c.a, c.g8 * c.a, c.b8 * c.a] + + +func _init_regex_patterns() -> void: + if not _tag_regex: + _tag_regex = RegEx.new() + # Match all richtext tags: [tag], [tag=value], [/tag] + _tag_regex.compile(r"\[/?(?:color|bgcolor|b|i|u)(?:=[^\]]+)?\]") + + +func _extract_color_from_tag(tag: String, tag_assign: String) -> Color: + var tag_assign_length := tag_assign.length() + var color_value := tag.substr(tag_assign_length, tag.length() - tag_assign_length - 1) + return _parse_color(color_value) + + +## Optimized richtext to CSI conversion using regex and lookup processing +func _bbcode_tags_to_csi_codes(message: String) -> String: + _init_regex_patterns() + + var result := "" + var last_pos := 0 + var color_stack: Array[Color] = [] + var bgcolor_stack: Array[Color] = [] + + # Find all richtext tags + var matches := _tag_regex.search_all(message) + + for match in matches: + var start_pos := match.get_start() + var end_pos := match.get_end() + var tag := match.get_string(0) + + # Add text before this tag + result += message.substr(last_pos, start_pos - last_pos) + + # Process the tag + if tag.begins_with("[color="): + var fg_color := _extract_color_from_tag(tag, "[color=") + color_stack.push_back(fg_color) + result += _color_to_csi_fg(fg_color) + elif tag.begins_with("[bgcolor="): + var bg_color := _extract_color_from_tag(tag, "[bgcolor=") + bgcolor_stack.push_back(bg_color) + result += _color_to_csi_bg(bg_color) + elif tag == "[b]": + result += CSI_BOLD + elif tag == "[i]": + result += CSI_ITALIC + elif tag == "[u]": + result += CSI_UNDERLINE + elif tag == "[/color]": + result += CSI_RESET + if color_stack.size() > 0: + color_stack.pop_back() + # Restore remaining styles and colors + if color_stack.size() > 0: + result += _color_to_csi_fg(color_stack[-1]) + if bgcolor_stack.size() > 0: + result += _color_to_csi_bg(bgcolor_stack[-1]) + elif tag == "[/bgcolor]": + result += CSI_RESET + if bgcolor_stack.size() > 0: + bgcolor_stack.pop_back() + # Restore remaining styles and colors + if color_stack.size() > 0: + result += _color_to_csi_fg(color_stack[-1]) + if bgcolor_stack.size() > 0: + result += _color_to_csi_bg(bgcolor_stack[-1]) + elif tag in ["[/b]", "[/i]", "[/u]"]: + result += CSI_RESET + # Restore remaining colors after style reset + if color_stack.size() > 0: + result += _color_to_csi_fg(color_stack[-1]) + if bgcolor_stack.size() > 0: + result += _color_to_csi_bg(bgcolor_stack[-1]) + + last_pos = end_pos + + # Add remaining text after last tag + result += message.substr(last_pos) + + return result + + +## Internal implementation of print_stack_trace.[br] +## [br] +## [param stack_trace] The stack trace to print.[br] +## [param _indent] The indentation level. +func _print_stack_trace(stack_trace: GdUnitStackTrace, _indent: int) -> void: + _print_message(stack_trace.print_stack_trace(), Color.LIGHT_BLUE, _indent, 0) + + +## Implementation of basic message output with formatting. +func _print_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + var text := _bbcode_tags_to_csi_codes(_message) + var indent_text := "".lpad(_indent * 2) + var _style := _apply_style_flags(_flags) + printraw("%s[38;2;%d;%d;%dm%s%s" % [indent_text, _color.r8, _color.g8, _color.b8, _style, text]) + _current_pos += _indent * 2 + text.length() + + +## Implementation of line-ending message output with formatting. +func _println_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + _print_message(_message, _color, _indent, _flags) + prints() + _current_pos = 0 + + +## Implementation of positioned message output with formatting. +func _print_at(_message: String, cursor_pos: int, _color: Color, _effect: GdUnitMessageWriter.Effect, _align: Align, _flags: int) -> void: + if _align == Align.RIGHT: + cursor_pos = cursor_pos - _message.length() + + if cursor_pos > _current_pos: + printraw("[%dG" % cursor_pos) # Move cursor to absolute position + else: + _message = " " + _message + + var _style := _apply_style_flags(_flags) + printraw("[38;2;%d;%d;%dm%s%s" % [_color.r8, _color.g8, _color.b8, _style, _message]) + _current_pos = cursor_pos + _message.length() + + +## Writes a line break and returns self for chaining. +func new_line() -> GdUnitCSIMessageWriter: + prints() + return self + + +## Saves the current cursor position.[br] +## Returns self for chaining. +func save_cursor() -> GdUnitCSIMessageWriter: + printraw("") + return self + + +## Restores previously saved cursor position.[br] +## Returns self for chaining. +func restore_cursor() -> GdUnitCSIMessageWriter: + printraw("") + return self + + +## Clears screen content and resets cursor position. +func clear() -> void: + printraw("") # Clear screen and move cursor to home + _current_pos = 0 + + +## Debug method to display the available color table.[br] +## Shows both 6x6x6 color cube and RGB color modes. +@warning_ignore("return_value_discarded") +func _print_color_table() -> void: + color(Color.ANTIQUE_WHITE).println_message("Color Table 6x6x6") + _debug_show_color_codes = true + for green in range(0, 6): + for red in range(0, 6): + for blue in range(0, 6): + color(Color8(red * 42, green * 42, blue * 42)).println_message("โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ ") + new_line() + new_line() + + color(Color.ANTIQUE_WHITE).println_message("Color Table RGB") + _color_mode = COLOR_RGB + for green in range(0, 6): + for red in range(0, 6): + for blue in range(0, 6): + color(Color8(red * 42, green * 42, blue * 42)).println_message("โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ ") + new_line() + new_line() + _color_mode = COLOR_TABLE + _debug_show_color_codes = false diff --git a/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid new file mode 100644 index 0000000..1a02af8 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://0qf2xrhqyw7c diff --git a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd new file mode 100644 index 0000000..d218ab9 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd @@ -0,0 +1,229 @@ +@tool +@abstract class_name GdUnitMessageWriter +extends RefCounted +## Base interface class for writing formatted messages to different outputs.[br] +## [br] +## This class defines the interface and common functionality for writing formatted messages.[br] +## It provides a fluent API for message formatting and supports different output targets.[br] +## [br] +## The class provides formatting options for:[br] +## - Text colors[br] +## - Text styles (bold, italic, underline)[br] +## - Text effects (e.g., wave)[br] +## - Text alignment[br] +## - Indentation[br] +## [br] +## Two concrete implementations are available:[br] +## - [GdUnitRichTextMessageWriter] writing to a [RichTextLabel][br] +## - [GdUnitCSIMessageWriter] writing to console using CSI codes[br] +## [br] +## Example usage:[br] +## [codeblock] +## writer.color(Color.RED).style(BOLD).println_message("Test failed!") +## writer.color(Color.GREEN).align(Align.RIGHT).print_at("Success", 80) +## [/codeblock] + + +## Text style flag for bold formatting +const BOLD = 0x1 +## Text style flag for italic formatting +const ITALIC = 0x2 +## Text style flag for underline formatting +const UNDERLINE = 0x4 + + +## Represents special text effects that can be applied to the output +enum Effect { + ## No special effect applied + NONE, + ## Applies a wave animation to the text + WAVE +} + + +## Controls text alignment at the specified cursor position +enum Align { + ## Aligns text to the left of the cursor position + LEFT, + ## Aligns text to the right of the cursor position, accounting for text length + RIGHT +} + + +## The current text color to be used for the next output operation +var _current_color := Color.WHITE + +## The current indentation level to be used for the next output operation.[br] +## Each level represents two spaces of indentation. +var _current_indent := 0 + +## The current text style flags (BOLD, ITALIC, UNDERLINE) to be used for the next output operation +var _current_flags := 0 + +## The current text alignment to be used for the next output operation +var _current_align := Align.LEFT + +## The current text effect to be used for the next output operation +var _current_effect := GdUnitMessageWriter.Effect.NONE + + +## Sets the text color for the next output operation.[br] +## [br] +## [param value] The color to be used for the text. +## Returns self for method chaining. +func color(value: Color) -> GdUnitMessageWriter: + _current_color = value + return self + + +## Sets the indentation level for the next output operation.[br] +## [br] +## [param value] The number of indentation levels, where each level equals two spaces. +## Returns self for method chaining. +func indent(value: int) -> GdUnitMessageWriter: + _current_indent = value + return self + + +## Sets text style flags for the next output operation.[br] +## [br] +## [param value] A combination of style flags (BOLD, ITALIC, UNDERLINE). +## Returns self for method chaining. +func style(value: int) -> GdUnitMessageWriter: + _current_flags = value + return self + + +## Sets text effect for the next output operation.[br] +## [br] +## [param value] The effect to apply to the text (NONE, WAVE). +## Returns self for method chaining. +func effect(value: GdUnitMessageWriter.Effect) -> GdUnitMessageWriter: + _current_effect = value + return self + + +## Sets text alignment for the next output operation.[br] +## [br] +## [param value] The alignment to use (LEFT, RIGHT). +## Returns self for method chaining. +func align(value: Align) -> GdUnitMessageWriter: + _current_align = value + return self + + +## Resets all formatting options to their default values.[br] +## [br] +## Defaults:[br] +## - color: Color.WHITE[br] +## - indent: 0[br] +## - flags: 0[br] +## - align: LEFT[br] +## - effect: NONE[br] +## Returns self for method chaining. +func reset() -> GdUnitMessageWriter: + _current_color = Color.WHITE + _current_indent = 0 + _current_flags = 0 + _current_align = Align.LEFT + _current_effect = Effect.NONE + return self + + +## Prints a warning message in golden color.[br] +## [br] +## [param message] The warning message to print. +func prints_warning(message: String) -> void: + color(Color.GOLDENROD).println_message(message) + + +## Prints an error message in crimson color.[br] +## [br] +## [param message] The error message to print. +func prints_error(message: String) -> void: + color(Color.CRIMSON).println_message(message) + + +## Prints a message with current formatting settings.[br] +## [br] +## [param message] The text to print. +func print_message(message: String) -> GdUnitMessageWriter: + _print_message(message, _current_color, _current_indent, _current_flags) + reset() + return self + + +## Prints a message with current formatting settings followed by a newline.[br] +## [br] +## [param message] The text to print. +func println_message(message: String) -> GdUnitMessageWriter: + _println_message(message, _current_color, _current_indent, _current_flags) + reset() + return self + + +## Prints a message at a specific column position with current formatting settings.[br] +## [br] +## [param message] The text to print.[br] +## [param cursor_pos] The column position where the text should start. +func print_at(message: String, cursor_pos: int) -> void: + _print_at(message, cursor_pos, _current_color, _current_effect, _current_align, _current_flags) + reset() + + +## Prints a stack trace with the current indentation setting.[br] +## [br] +## [param stack_trace] The stack trace to print. +func print_stack_trace(stack_trace: GdUnitStackTrace) -> void: + if stack_trace: + _print_stack_trace(stack_trace, _current_indent) + + +## Internal implementation of print_stack_trace.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param stack_trace] The stack trace to print.[br] +## [param current_indent] The indentation level. +@abstract func _print_stack_trace(stack_trace: GdUnitStackTrace, current_indent: int) -> void + + +## Internal implementation of print_message.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param _message] The text to print.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param _flags] The style flags to apply. +@abstract func _print_message(_message: String, _color: Color, _indent: int, _flags: int) -> void + + +## Internal implementation of println_message.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param _message] The text to print.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param _flags] The style flags to apply. +@abstract func _println_message(_message: String, _color: Color, _indent: int, _flags: int) -> void + + +## Internal implementation of print_at.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param _message] The text to print.[br] +## [param _cursor_pos] The column position.[br] +## [param _color] The color to use.[br] +## [param _effect] The effect to apply.[br] +## [param _align] The text alignment.[br] +## [param _flags] The style flags to apply. +@abstract func _print_at(_message: String, _cursor_pos: int, _color: Color, _effect: GdUnitMessageWriter.Effect, _align: Align, _flags: int) -> void + + +## Clears all output content.[br] +## [br] +## To be overridden by concrete formatters. +@abstract func clear() -> void diff --git a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid new file mode 100644 index 0000000..906ac9e --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://bmr2exij3nemn diff --git a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd new file mode 100644 index 0000000..523dd04 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd @@ -0,0 +1,129 @@ +@tool +class_name GdUnitRichTextMessageWriter +extends GdUnitMessageWriter +## A message writer implementation using [RichTextLabel] for the test report UI.[br] +## [br] +## This writer implementation writes formatted messages to a [RichTextLabel] using BBCode.[br] +## It supports:[br] +## - Text formatting using BBCode (bold, italic, underline)[br] +## - Text coloring using push colors[br] +## - Text indentation using push indent[br] +## - Text effects like wave[br] +## - Basic cursor positioning[br] +## [br] +## Used to format test reports in the editor UI. + + +## The [RichTextLabel] instance to write formatted messages +var _output: RichTextLabel +var _report_formatter: GdUnitReportPanel + +## Tracks current position in characters from line start +var _current_pos := 0 + + +## Creates a new message writer for the given [RichTextLabel].[br] +## [br] +## [param output] The [RichTextLabel] used for output. +func _init(output: RichTextLabel) -> void: + _output = output + _report_formatter = GdUnitReportPanel.new() + + +## Applies text style flags by wrapping text in BBCode tags.[br] +## [br] +## Available styles:[br] +## - BOLD: [b]text[/b][br] +## - ITALIC: [i]text[/i][br] +## - UNDERLINE: [u]text[/u][br] +## [br] +## [param message] The text to format.[br] +## [param flags] The text style flags to apply. +func _apply_flags(message: String, flags: int) -> String: + if flags & BOLD: + message = "[b]%s[/b]" % message + if flags & ITALIC: + message = "[i]%s[/i]" % message + if flags & UNDERLINE: + message = "[u]%s[/u]" % message + return message + + +## Internal implementation of print_stack_trace.[br] +## [br] +## [param stack_trace] The stack trace to print.[br] +## [param _indent] The indentation level. +func _print_stack_trace(stack_trace: GdUnitStackTrace, _indent: int) -> void: + for i in _indent: + _output.push_indent(1) + _report_formatter.add_stack_trace(_output, stack_trace) + for i in _indent: + _output.pop() + + +## Writes a message with formatting.[br] +## [br] +## [param message] The text to write.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param flags] The text style flags to apply. +func _print_message(message: String, _color: Color, _indent: int, flags: int) -> void: + for i in _indent: + _output.push_indent(1) + _output.push_color(_color) + message = _apply_flags(message, flags) + _output.append_text(message) + _output.pop() + for i in _indent: + _output.pop() + _current_pos += _indent * 2 + message.length() + + +## Writes a message with formatting followed by a line break.[br] +## [br] +## [param message] The text to write.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param flags] The text style flags to apply. +func _println_message(message: String, _color: Color, _indent: int, flags: int) -> void: + _print_message(message, _color, _indent, flags) + _output.newline() + _current_pos = 0 + + +## Writes a message at a specific column position.[br] +## [br] +## [param message] The text to write.[br] +## [param cursor_pos] The column position from line start.[br] +## [param _color] The color to use.[br] +## [param _effect] The text effect to apply (e.g. wave).[br] +## [param _align] The text alignment (left or right).[br] +## [param flags] The text style flags to apply. +func _print_at(message: String, cursor_pos: int, _color: Color, _effect: GdUnitMessageWriter.Effect, _align: Align, flags: int) -> void: + if _align == Align.RIGHT: + cursor_pos = cursor_pos - message.length() + + var spaces := cursor_pos - _current_pos + if spaces > 0: + _output.append_text("".lpad(spaces)) + _current_pos += spaces + else: + _output.append_text(" ") + _current_pos += 1 + + _output.push_color(_color) + message = _apply_flags(message, flags) + match _effect: + Effect.NONE: + pass + Effect.WAVE: + message = "[wave]%s[/wave]" % message + _output.append_text(message) + _output.pop() + _current_pos += message.length() + + +## Clears all written content from the [RichTextLabel]. +func clear() -> void: + _output.clear() + _current_pos = 0 diff --git a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid new file mode 100644 index 0000000..f0689f8 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://dmdgiqvgxrwg6 diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs new file mode 100644 index 0000000..096e83c --- /dev/null +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs @@ -0,0 +1,235 @@ +// Copyright (c) 2025 Mike Schulze +// MIT License - See LICENSE file in the repository root for full license text +#pragma warning disable IDE1006 +namespace gdUnit4.addons.gdUnit4.src.dotnet; +#pragma warning restore IDE1006 + +#if GDUNIT4NET_API_V5 +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using GdUnit4; +using GdUnit4.Api; + +using Godot; +using Godot.Collections; + +/// +/// The GdUnit4 GDScript - C# API wrapper. +/// +public partial class GdUnit4CSharpApi : RefCounted +{ + /// + /// The signal to be emitted when the execution is completed. + /// + [Signal] +#pragma warning disable CA1711 + public delegate void ExecutionCompletedEventHandler(); +#pragma warning restore CA1711 + +#pragma warning disable CA2213, SA1201 + private CancellationTokenSource? executionCts; +#pragma warning restore CA2213, SA1201 + + /// + /// Indicates if the API loaded. + /// + /// Returns true if the API already loaded. + public static bool IsApiLoaded() + => true; + + /// + /// Runs test discovery on the given script. + /// + /// The script to be scanned. + /// The list of tests discovered as dictionary. + public static Array DiscoverTests(CSharpScript sourceScript) + { + try + { + // Get the list of test case descriptors from the API + var testCaseDescriptors = GdUnit4NetApiGodotBridge.DiscoverTestsFromScript(sourceScript); + + // Convert each TestCaseDescriptor to a Dictionary + return testCaseDescriptors + .Select(descriptor => new Dictionary + { + ["guid"] = descriptor.Id.ToString(), + ["managed_type"] = descriptor.ManagedType, + ["test_name"] = descriptor.ManagedMethod, + ["source_file"] = sourceScript.ResourcePath, + ["line_number"] = descriptor.LineNumber, + ["attribute_index"] = descriptor.AttributeIndex, + ["require_godot_runtime"] = descriptor.RequireRunningGodotEngine, + ["code_file_path"] = descriptor.CodeFilePath ?? string.Empty, + ["simple_name"] = descriptor.SimpleName, + ["fully_qualified_name"] = descriptor.FullyQualifiedName, + ["assembly_location"] = descriptor.AssemblyPath + }) + .Aggregate( + new Array(), + (array, dict) => + { + array.Add(dict); + return array; + }); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + GD.PrintErr($"Error discovering tests: {e.Message}\n{e.StackTrace}"); +#pragma warning disable IDE0028 // Do not catch general exception types + return new Array(); +#pragma warning restore IDE0028 // Do not catch general exception types + } + } + + /// + /// Creates a test suite based on the specified source path and line number. + /// + /// The path to the source file from which to create the test suite. + /// The line number in the source file where the method to test is defined. + /// The path where the test suite should be created. + /// A dictionary containing information about the created test suite. + public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) + => GdUnit4NetApiGodotBridge.CreateTestSuite(sourcePath, lineNumber, testSuitePath); + + /// + /// Gets the version of the GdUnit4 assembly. + /// + /// The version string of the GdUnit4 assembly. + public static string Version() + => GdUnit4NetApiGodotBridge.Version(); + + /// + public override void _Notification(int what) + { + if (what != NotificationPredelete) + return; + executionCts?.Dispose(); + executionCts = null; + } + + /// + /// Executes the tests and using the listener for reporting the results. + /// + /// A list of tests to be executed. + /// The listener to report the results. + public void ExecuteAsync(Array tests, Callable listener) + { + try + { + // Cancel any ongoing execution + executionCts?.Cancel(); + executionCts?.Dispose(); + + // Create new cancellation token source + executionCts = new CancellationTokenSource(); + + Debug.Assert(tests != null, nameof(tests) + " != null"); + var testSuiteNodes = new List { BuildTestSuiteNodeFrom(tests) }; + GdUnit4NetApiGodotBridge.ExecuteAsync(testSuiteNodes, listener, executionCts.Token) + .GetAwaiter() + .OnCompleted(() => EmitSignal(SignalName.ExecutionCompleted)); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + GD.PrintErr($"Error executing tests: {e.Message}\n{e.StackTrace}"); + Task.Run(() => { }).GetAwaiter().OnCompleted(() => EmitSignal(SignalName.ExecutionCompleted)); + } + } + + /// + /// Will cancel the current test execution. + /// + public void CancelExecution() + { + try + { + executionCts?.Cancel(); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + GD.PrintErr($"Error cancelling execution: {e.Message}"); + } + } + + // Convert a set of Tests stored as Dictionaries to TestSuiteNode + // all tests are assigned to a single test suit + internal static TestSuiteNode BuildTestSuiteNodeFrom(Array tests) + { + if (tests.Count == 0) + throw new InvalidOperationException("Cant build 'TestSuiteNode' from an empty test set."); + + // Create a suite ID + var suiteId = Guid.NewGuid(); + var firstTest = tests[0]; + var managedType = firstTest["managed_type"].AsString(); + var assemblyLocation = firstTest["assembly_location"].AsString(); + var sourceFile = firstTest["source_file"].AsString(); + + // Create TestCaseNodes for each test in the suite + var testCaseNodes = tests + .Select(test => new TestCaseNode + { + Id = Guid.Parse(test["guid"].AsString()), + ParentId = suiteId, + ManagedMethod = test["test_name"].AsString(), + LineNumber = test["line_number"].AsInt32(), + AttributeIndex = test["attribute_index"].AsInt32(), + RequireRunningGodotEngine = test["require_godot_runtime"].AsBool() + }) + .ToList(); + + return new TestSuiteNode + { + Id = suiteId, + ParentId = Guid.Empty, + ManagedType = managedType, + AssemblyPath = assemblyLocation, + SourceFile = sourceFile, + Tests = testCaseNodes + }; + } +} +#else +using Godot; +using Godot.Collections; + +public partial class GdUnit4CSharpApi : RefCounted +{ + [Signal] + public delegate void ExecutionCompletedEventHandler(); + + public static bool IsApiLoaded() + { + GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); + return false; + } + + + public static string Version() + => "Unknown"; + + public static Array DiscoverTests(CSharpScript sourceScript) => new(); + + public void ExecuteAsync(Array tests, Callable listener) + { + } + + public static bool IsTestSuite(CSharpScript script) + => false; + + public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) + => new(); +} +#endif diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd new file mode 100644 index 0000000..3d8ba25 --- /dev/null +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd @@ -0,0 +1,114 @@ +## GdUnit4CSharpApiLoader +## +## A bridge class that handles communication between GDScript and C# for the GdUnit4 testing framework. +## This loader acts as a compatibility layer to safely access the .NET API and ensure that calls +## only proceed when the .NET environment is properly configured and available. +## [br] +## The class handles: +## - Verification of .NET runtime availability +## - Loading the C# wrapper script +## - Checking for the GdUnit4Api assembly +## - Providing proxy methods to access GdUnit4 functionality in C# +@static_unload +class_name GdUnit4CSharpApiLoader +extends RefCounted + +## Cached reference to the loaded C# wrapper script +static var _gdUnit4NetWrapper: Script + +## Cached instance of the API (singleton pattern) +static var _api_instance: RefCounted + + +class TestEventListener extends RefCounted: + + func publish_event(event: Dictionary) -> void: + var test_event := GdUnitEvent.new().deserialize(event) + GdUnitSignals.instance().gdunit_event.emit(test_event) + +static var _test_event_listener := TestEventListener.new() + + +## Returns an instance of the GdUnit4CSharpApi wrapper.[br] +## @return Script: The loaded C# wrapper or null if .NET is not supported +static func instance() -> Script: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return null + + return _gdUnit4NetWrapper + + +## Returns or creates a single instance of the API [br] +## This improves performance by reusing the same object +static func api_instance() -> RefCounted: + if _api_instance == null and is_api_loaded(): + @warning_ignore("unsafe_method_access") + _api_instance = instance().new() + return _api_instance + + +static func is_engine_version_supported(engine_version: int = Engine.get_version_info().hex) -> bool: + return engine_version >= 0x40200 + + +## Checks if the .NET environment is properly configured and available.[br] +## @return bool: True if .NET is fully supported and the assembly is found +static func is_api_loaded() -> bool: + # If the wrapper is already loaded we don't need to check again + if _gdUnit4NetWrapper != null: + return true + + # First we check if this is a Godot .NET runtime instance + if not ClassDB.class_exists("CSharpScript") or not is_engine_version_supported(): + return false + # Second we check the C# project file exists + var assembly_name: String = ProjectSettings.get_setting("dotnet/project/assembly_name") + if assembly_name.is_empty() or not FileAccess.file_exists("res://%s.csproj" % assembly_name): + return false + + # Finally load the wrapper and check if the GdUnit4 assembly can be found + _gdUnit4NetWrapper = load("res://addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs") + @warning_ignore("unsafe_method_access") + return _gdUnit4NetWrapper.call("IsApiLoaded") + + +## Returns the version of the GdUnit4 .NET assembly.[br] +## @return String: The version string or "unknown" if .NET is not supported +static func version() -> String: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return "unknown" + @warning_ignore("unsafe_method_access") + return instance().Version() + + +static func discover_tests(source_script: Script) -> Array[GdUnitTestCase]: + var tests: Array = _gdUnit4NetWrapper.call("DiscoverTests", source_script) + + return Array(tests.map(GdUnitTestCase.from_dict), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + +static func execute(tests: Array[GdUnitTestCase]) -> void: + var net_api := api_instance() + if net_api == null: + push_warning("Execute C# tests not supported!") + return + var tests_as_dict: Array[Dictionary] = Array(tests.map(GdUnitTestCase.to_dict), TYPE_DICTIONARY, "", null) + + net_api.call("ExecuteAsync", tests_as_dict, _test_event_listener.publish_event) + @warning_ignore("unsafe_property_access") + await net_api.ExecutionCompleted + + +static func create_test_suite(source_path: String, line_number: int, test_suite_path: String) -> GdUnitResult: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return GdUnitResult.error("Can't create test suite. No .NET support found.") + @warning_ignore("unsafe_method_access") + var result: Dictionary = instance().CreateTestSuite(source_path, line_number, test_suite_path) + if result.has("error"): + return GdUnitResult.error(str(result.get("error"))) + return GdUnitResult.success(result) + + +static func is_csharp_file(resource_path: String) -> bool: + var ext := resource_path.get_extension() + return ext == "cs" and GdUnit4CSharpApiLoader.is_api_loaded() diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid new file mode 100644 index 0000000..e911163 --- /dev/null +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid @@ -0,0 +1 @@ +uid://ds4xwdcjn2quy diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd b/addons/gdUnit4/src/doubler/CallableDoubler.gd new file mode 100644 index 0000000..1bc78a1 --- /dev/null +++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd @@ -0,0 +1,156 @@ +## The helper class to allow to double Callable +## Is just a wrapper to the original callable with the same function signature. +## +## Due to interface conflicts between 'Callable' and 'Object', +## it is not possible to stub the 'call' and 'call_deferred' methods. +## +## The Callable interface and the Object class have overlapping method signatures, +## which causes conflicts when attempting to stub these methods. +## As a result, you cannot create stubs for 'call' and 'call_deferred' methods. + +class_name CallableDoubler + + +const doubler_script :Script = preload("res://addons/gdUnit4/src/doubler/CallableDoubler.gd") + +var _cb: Callable + + +func _init(cb: Callable) -> void: + assert(cb!=null, "Invalid argument must not be null") + _cb = cb + +## --- helpers ----------------------------------------------------------------------------------------------------------------------------- +static func map_func_name(method_info: Dictionary) -> String: + return method_info["name"] + + +## We do not want to double all functions based on Object for this class +## Is used on SpyBuilder to excluding functions to be doubled for Callable +static func excluded_functions() -> PackedStringArray: + return ClassDB.class_get_method_list("Object")\ + .map(CallableDoubler.map_func_name)\ + .filter(func (name: String) -> bool: + return !CallableDoubler.callable_functions().has(name)) + + +static func non_callable_functions(name: String) -> bool: + return ![ + # we allow "_init", is need to construct it, + "excluded_functions", + "non_callable_functions", + "callable_functions", + "map_func_name" + ].has(name) + + +## Returns the list of supported Callable functions +static func callable_functions() -> PackedStringArray: + var supported_functions :Array = doubler_script.get_script_method_list()\ + .map(CallableDoubler.map_func_name)\ + .filter(CallableDoubler.non_callable_functions) + # We manually add these functions that we cannot/may not overwrite in this class + supported_functions.append_array(["call_deferred", "callv"]) + return supported_functions + + +## ----------------------------------------------------------------------------------------------------------------------------------------- +## Callable functions stubing +## ----------------------------------------------------------------------------------------------------------------------------------------- + +func bind(...varargs: Array) -> Callable: + _cb = _cb.bindv(varargs) + return _cb + + +func bindv(caller_args: Array) -> Callable: + _cb = _cb.bindv(caller_args) + return _cb + + +@warning_ignore("native_method_override") +func call(...varargs: Array) -> Variant: + return _cb.callv(varargs) + + +# Is not supported, see class description +#func call_deferred(...varargs: Array) -> void: +# return _cb.call_deferred(varargs) + + +# Is not supported, see class description +#func callv(arguments: Array) -> Variant: +# return _cb.callv(arguments) + + +func get_bound_arguments() -> Array: + return _cb.get_bound_arguments() + + +func get_bound_arguments_count() -> int: + return _cb.get_bound_arguments_count() + + +func get_method() -> StringName: + return _cb.get_method() + + +func get_object() -> Object: + return _cb.get_object() + + +func get_object_id() -> int: + return _cb.get_object_id() + + +func hash() -> int: + return _cb.hash() + + +func is_custom() -> bool: + return _cb.is_custom() + + +func is_null() -> bool: + return _cb.is_null() + + +func is_standard() -> bool: + return _cb.is_standard() + + +func is_valid() -> bool: + return _cb.is_valid() + + +func rpc(...varargs: Array) -> void: + match varargs.size(): + 0: _cb.rpc() + 1: _cb.rpc(varargs[0]) + 2: _cb.rpc(varargs[0], varargs[1]) + 3: _cb.rpc(varargs[0], varargs[1], varargs[2]) + 4: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) + 5: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) + 6: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) + 7: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) + 8: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) + 9: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) + + +@warning_ignore("untyped_declaration") +func rpc_id(peer_id: int, ...varargs: Array) -> void: + match varargs.size(): + 0: _cb.rpc_id(peer_id ) + 1: _cb.rpc_id(peer_id, varargs[0]) + 2: _cb.rpc_id(peer_id, varargs[0], varargs[1]) + 3: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2]) + 4: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) + 5: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) + 6: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) + 7: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) + 8: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) + 9: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) + +func unbind(argcount: int) -> Callable: + _cb = _cb.unbind(argcount) + return _cb diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid new file mode 100644 index 0000000..bc269cb --- /dev/null +++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid @@ -0,0 +1 @@ +uid://byho46gi0f7h5 diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd new file mode 100644 index 0000000..d9459dc --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd @@ -0,0 +1,4 @@ +@abstract class_name GdFunctionDoubler +extends RefCounted + +@abstract func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid new file mode 100644 index 0000000..90b9b10 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://dtpfrhxduvod1 diff --git a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd new file mode 100644 index 0000000..31aa06b --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd @@ -0,0 +1,119 @@ +# A class doubler used to mock and spy checked implementations +class_name GdUnitClassDoubler +extends RefCounted + +const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_" +const EXCLUDE_VIRTUAL_FUNCTIONS = [ + # we have to exclude notifications because NOTIFICATION_PREDELETE is try + # to delete already freed spy/mock resources and will result in a conflict + "_notification", + "notification", + # https://github.com/godotengine/godot/issues/67461 + "get_name", + "get_path", + "duplicate", + ] +# define functions to be exclude when spy or mock checked a scene +const EXLCUDE_SCENE_FUNCTIONS = [ + # needs to exclude get/set script functions otherwise it endsup in recursive endless loop + "set_script", + "get_script", + # needs to exclude otherwise verify fails checked collection arguments checked calling to string + "_to_string", +] +const EXCLUDE_FUNCTIONS = ["new", "free", "get_instance_id", "get_tree"] + + +static func check_leaked_instances() -> void: + ## we check that all registered spy/mock instances are removed from the engine meta data + for key in Engine.get_meta_list(): + if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX): + var instance: Variant = Engine.get_meta(key) + push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS]) + await (Engine.get_main_loop() as SceneTree).process_frame + + +# loads the doubler template +# class_info = { "class_name": <>, "class_path" : <>} +static func load_template(template: String, class_info: Dictionary) -> PackedStringArray: + var clazz_name: String = class_info.get("class_name") + var source_code := template\ + .replace("${source_class}", clazz_name)\ + # Replace template class_name DoubledClass with source class name + .replace("SourceClassName", clazz_name.replace(".", "_")) + var lines := GdScriptParser.to_unix_format(source_code).split("\n") + @warning_ignore("return_value_discarded") + lines.insert(1, extends_clazz(class_info)) + lines.insert(0, "@warning_ignore_start('unsafe_call_argument', 'shadowed_variable', 'untyped_declaration', 'native_method_override', 'int_as_enum_without_cast')") + return lines + + +static func extends_clazz(class_info: Dictionary) -> String: + var clazz_name: String = class_info.get("class_name") + var clazz_path: PackedStringArray = class_info.get("class_path", []) + # is inner class? + if clazz_path.size() > 1: + return "extends %s" % clazz_name + if clazz_path.size() == 1 and clazz_path[0].ends_with(".gd"): + return "extends '%s'" % clazz_path[0] + return "extends %s" % clazz_name + + +# double all functions of given instance +static func double_functions(instance: Object, clazz_name: String, clazz_path: PackedStringArray, func_doubler: GdFunctionDoubler, exclude_functions: Array) -> PackedStringArray: + var doubled_source := PackedStringArray() + var parser := GdScriptParser.new() + var exclude_override_functions := EXCLUDE_VIRTUAL_FUNCTIONS + EXCLUDE_FUNCTIONS + exclude_functions + var functions := Array() + + # double script functions + if not ClassDB.class_exists(clazz_name): + var result := parser.parse(clazz_name, clazz_path) + if result.is_error(): + push_error(result.error_message()) + return PackedStringArray() + var class_descriptor: GdClassDescriptor = result.value() + for func_descriptor in class_descriptor.functions(): + if instance != null and not instance.has_method(func_descriptor.name()): + #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) + continue + if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): + continue + doubled_source += func_doubler.double(func_descriptor) + functions.append(func_descriptor.name()) + + # double regular class functions + var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path) + for method: Dictionary in clazz_functions: + var func_descriptor := GdFunctionDescriptor.extract_from(method) + # exclude private core functions + if func_descriptor.is_private(): + continue + if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): + continue + # GD-110: Hotfix do not double invalid engine functions + if is_invalid_method_descriptior(method): + #prints("'%s': invalid method descriptor found! %s" % [clazz_name, method]) + continue + # do not double on not implemented virtual functions + if instance != null and not instance.has_method(func_descriptor.name()): + #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) + continue + functions.append(func_descriptor.name()) + doubled_source.append_array(func_doubler.double(func_descriptor)) + return doubled_source + + +# GD-110 +static func is_invalid_method_descriptior(method: Dictionary) -> bool: + var return_info: Dictionary = method["return"] + var type: int = return_info["type"] + var usage: int = return_info["usage"] + var clazz_name: String = return_info["class_name"] + # is method returning a type int with a given 'class_name' we have an enum + # and the PROPERTY_USAGE_CLASS_IS_ENUM must be set + if type == TYPE_INT and not clazz_name.is_empty() and not (usage & PROPERTY_USAGE_CLASS_IS_ENUM): + return true + if clazz_name == "Variant.Type": + return true + return false diff --git a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid new file mode 100644 index 0000000..94a5dbd --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid @@ -0,0 +1 @@ +uid://dje38pdtf6oxr diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd new file mode 100644 index 0000000..a1a75f1 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd @@ -0,0 +1,336 @@ +class_name GdUnitFunctionDoublerBuilder +extends RefCounted + +const TYPE_VOID = GdObjects.TYPE_VOID +const TYPE_VARIANT = GdObjects.TYPE_VARIANT +const TYPE_VARARG = GdObjects.TYPE_VARARG +const TYPE_FUNC = GdObjects.TYPE_FUNC +const TYPE_FUZZER = GdObjects.TYPE_FUZZER +const TYPE_ENUM = GdObjects.TYPE_ENUM + +const DEFAULT_TYPED_RETURN_VALUES := { + TYPE_NIL: "null", + TYPE_BOOL: "false", + TYPE_INT: "0", + TYPE_FLOAT: "0.0", + TYPE_STRING: "\"\"", + TYPE_STRING_NAME: "&\"\"", + TYPE_VECTOR2: "Vector2.ZERO", + TYPE_VECTOR2I: "Vector2i.ZERO", + TYPE_RECT2: "Rect2()", + TYPE_RECT2I: "Rect2i()", + TYPE_VECTOR3: "Vector3.ZERO", + TYPE_VECTOR3I: "Vector3i.ZERO", + TYPE_VECTOR4: "Vector4.ZERO", + TYPE_VECTOR4I: "Vector4i.ZERO", + TYPE_TRANSFORM2D: "Transform2D()", + TYPE_PLANE: "Plane()", + TYPE_QUATERNION: "Quaternion()", + TYPE_AABB: "AABB()", + TYPE_BASIS: "Basis()", + TYPE_TRANSFORM3D: "Transform3D()", + TYPE_PROJECTION: "Projection()", + TYPE_COLOR: "Color()", + TYPE_NODE_PATH: "NodePath()", + TYPE_RID: "RID()", + TYPE_OBJECT: "null", + 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()", + GdObjects.TYPE_VARIANT: "null", + GdObjects.TYPE_ENUM: "0" +} + + +# @GlobalScript enums +# needs to manually map because of https://github.com/godotengine/godot/issues/73835 +const DEFAULT_ENUM_RETURN_VALUES = { + "Side" : "SIDE_LEFT", + "Corner" : "CORNER_TOP_LEFT", + "Orientation" : "HORIZONTAL", + "ClockDirection" : "CLOCKWISE", + "HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT", + "VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP", + "InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO", + "EulerOrder" : "EULER_ORDER_XYZ", + "Key" : "KEY_NONE", + "KeyModifierMask" : "KEY_CODE_MASK", + "MouseButton" : "MOUSE_BUTTON_NONE", + "MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT", + "JoyButton" : "JOY_BUTTON_INVALID", + "JoyAxis" : "JOY_AXIS_INVALID", + "MIDIMessage" : "MIDI_MESSAGE_NONE", + "Error" : "OK", + "PropertyHint" : "PROPERTY_HINT_NONE", + "Variant.Type" : "TYPE_NIL", + "Vector2.Axis" : "Vector2.AXIS_X", + "Vector2i.Axis" : "Vector2i.AXIS_X", + "Vector3.Axis" : "Vector3.AXIS_X", + "Vector3i.Axis" : "Vector3i.AXIS_X", + "Vector4.Axis" : "Vector4.AXIS_X", + "Vector4i.Axis" : "Vector4i.AXIS_X", +} + + +static var def_constructor := """ + func _init({constructor_args}) -> void: + __init_doubler() + super({args}) + """.dedent() + + +static var def_verify_block := """ + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("{func_name}", __args) + {default_return} + else: + __verifier.save_function_interaction("{func_name}", __args) + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_prepare_block := """ + if __is_prepare_return_value(): + __save_function_return_value("{func_name}", __args) + {default_return} + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_void_prepare_block := """ + if __is_prepare_return_value(): + push_error("Mocking functions with return type void is not allowed!") + return + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_mock_return := """ + if __is_do_not_call_real_func("{func_name}", __args): + return __return_mock_value("{func_name}", __args, {default_return}) + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_void_mock_return := """ + if __is_do_not_call_real_func("{func_name}", __args): + return + """.dedent().indent("\t").trim_suffix("\n") + + +var fd: GdFunctionDescriptor +var func_args: Array +var default_return: String +var verify_block: String = "" +var prepare_block: String = "" +var mock_return: String = "" + + +func _init(descriptor: GdFunctionDescriptor) -> void: + # verify all default types are covered + for type_key in TYPE_MAX: + if not DEFAULT_TYPED_RETURN_VALUES.has(type_key): + push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX]) + prints("missing default definition for type", type_key) + assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!") + + fd = descriptor + func_args = argument_names() + default_return = default_return_value() + + +func build_func_signature() -> String: + var return_type := ":" if fd._return_type == TYPE_VARIANT else " -> %s:" % fd.return_type_as_string() + return "{static}func {func_name}({args}){return_type}".format({ + "static" : "static " if fd.is_static() else "", + "func_name": fd.name(), + "args": arguments_full_quilified(), + "return_type": return_type + }) + + +func arguments_full_quilified() -> String: + var collect := PackedStringArray() + for arg in fd.args(): + var name := argument_name(arg) + if arg.has_default(): + var signature := "{argument_name}{arg_typed}={arg_value}".format({ + "argument_name" : name, + "arg_typed" : ":"+GdObjects.type_as_string(arg.type()) if arg.type() == GdObjects.TYPE_VARIANT else "", + "arg_value" : arg.value_as_string() + }) + collect.push_back(signature) + else: + collect.push_back(name) + if fd.is_vararg(): + var arg_descriptor := fd.varargs()[0] + collect.push_back("...%s_: Array" % arg_descriptor.name()) + return ", ".join(collect) + + +func argument_name(arg: GdFunctionArgument) -> String: + return arg.name() + "_" + + +func argument_names() -> PackedStringArray: + return fd.args().map(argument_name) + + +func argument_default(arg :GdFunctionArgument) -> String: + return (arg.value_as_string() + if arg.has_default() + else DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null")) + + +func build_constructor_arguments() -> String: + var arguments := PackedStringArray() + for arg in fd.args(): + var default_value := argument_default(arg) + var arg_signature := "{name}:{type}={default}".format({ + "name" : argument_name(arg), + "type" : "Variant" if default_value == "null" else "", + "default" : default_value + }) + arguments.append(arg_signature) + if fd.is_vararg(): + arguments.append("...varargs: Array") + return ", ".join(arguments) + + +func build_arguments() -> String: + return "\tvar __args := [{args}]{varargs}".format({ + "args" : ", ".join(func_args), + "varargs" : " + varargs_" if fd.is_vararg() else "" + }) + + +func build_super_calls() -> String: + if !fd.is_vararg(): + return 'super(%s)\n' % ", ".join(func_args) + + var match_block := "match varargs_.size():\n" + for index in range(0, 11): + match_block += '{index}: super({args})\n'.format({ + "index" : index, + "args" : ", ".join(func_args + build_vararg_list(index)) + }).indent("\t") + match_block += '_: push_error("To many varradic arguments.")\n'.indent("\t") + match_block += "return\n" if is_void_func() else "return %s\n" % default_return + return match_block + + +func build_vararg_list(count: int) -> Array: + var arg_list := [] + for index in count: + arg_list.append("varargs_[%d]" % index) + return arg_list + + +func default_return_value() -> String: + var return_type: Variant = fd.return_type() + if return_type == GdObjects.TYPE_ENUM: + var enum_class := fd._return_class + if DEFAULT_ENUM_RETURN_VALUES.has(enum_class): + return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0") + + var enum_path := enum_class.split(".") + if enum_path.size() >= 2: + var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1]) + if not keys.is_empty(): + return "%s.%s" % [enum_path[0], keys[0]] + var enum_value: Variant = get_enum_default(enum_class) + if enum_value != null: + return str(enum_value) + # we need fallback for @GlobalScript enums, + return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0") + return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid") + + +# Determine the enum default by reflection +func get_enum_default(value: String) -> Variant: + var script := GDScript.new() + script.source_code = """ + extends RefCounted + + static func get_enum_default() -> Variant: + return %s.values()[0] + + """.dedent() % value + var err := script.reload() + if err != OK: + push_error("Cant get enum values form '%s', %s" % [value, error_string(err)]) + return 0 + @warning_ignore("unsafe_method_access") + return script.new().call("get_enum_default") + + +func is_void_func() -> bool: + return fd.return_type() == TYPE_NIL or fd.return_type() == TYPE_VOID + + +func with_verify_block() -> GdUnitFunctionDoublerBuilder: + verify_block = def_verify_block.format({ + "func_name" : fd.name(), + "default_return" : "return" if is_void_func() else "return " + default_return + }) + return self + + +func with_prepare_block() -> GdUnitFunctionDoublerBuilder: + if fd.return_type() == TYPE_NIL or fd.return_type() == GdObjects.TYPE_VOID: + prepare_block = def_void_prepare_block + return self + + prepare_block = def_prepare_block.format({ + "func_name" : fd.name(), + "default_return" : "return" if is_void_func() else "return " + default_return + }) + return self + + +func with_mocked_return_value() -> GdUnitFunctionDoublerBuilder: + if is_void_func(): + mock_return = def_void_mock_return.format({ + "func_name" : fd.name(), + }) + else: + mock_return = def_mock_return.format({ + "func_name" : fd.name(), + "default_return" : '"no_arg"' if is_void_func() else default_return + }) + return self + + +func build() -> PackedStringArray: + if fd.name() == "_init": + return [def_constructor.format({ + "constructor_args" : build_constructor_arguments(), + "args" : ", ".join(func_args) + })] + + var func_body: PackedStringArray = [] + func_body.append(build_func_signature()) + func_body.append(build_arguments()) + if not prepare_block.is_empty(): + func_body.append(prepare_block) + func_body.append(verify_block) + if not mock_return.is_empty(): + func_body.append(mock_return) + func_body.append("") + var super_calls := build_super_calls() + if not is_void_func(): + super_calls = super_calls.replace("super(", "return super(" ) + if fd.is_coroutine(): + super_calls = super_calls.replace("super(", "await super(" ) + func_body.append(super_calls.indent("\t")) + return func_body diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid new file mode 100644 index 0000000..62d277a --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid @@ -0,0 +1 @@ +uid://bma77oere2n0w diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd new file mode 100644 index 0000000..d735bc5 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd @@ -0,0 +1,10 @@ +class_name GdUnitMockFunctionDoubler +extends GdFunctionDoubler + + +func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray: + return GdUnitFunctionDoublerBuilder.new(func_descriptor)\ + .with_prepare_block()\ + .with_verify_block()\ + .with_mocked_return_value()\ + .build() diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid new file mode 100644 index 0000000..b8347da --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://cjdw0wotni1u1 diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd new file mode 100644 index 0000000..789eb32 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd @@ -0,0 +1,53 @@ +class_name GdUnitObjectInteractions +extends RefCounted + + +static func verify(interaction_object: Object, interactions_times: int) -> Variant: + if not _is_mock_or_spy(interaction_object): + return interaction_object + + _get_verifier(interaction_object).do_verify_interactions(interactions_times) + return interaction_object + + +static func verify_no_interactions(interaction_object: Object) -> GdUnitAssert: + var assert_tool := GdUnitAssertImpl.new("") + if not _is_mock_or_spy(interaction_object): + return assert_tool.report_success() + + var summary := _get_verifier(interaction_object).verify_no_interactions() + if summary.is_empty(): + return assert_tool.report_success() + return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary)) + + +static func verify_no_more_interactions(interaction_object: Object) -> GdUnitAssert: + var assert_tool := GdUnitAssertImpl.new("") + if not _is_mock_or_spy(interaction_object): + return assert_tool + + var summary := _get_verifier(interaction_object).verify_no_more_interactions() + if summary.is_empty(): + return assert_tool + return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary)) + + +static func reset(interaction_object: Object) -> Object: + if not _is_mock_or_spy(interaction_object): + return interaction_object + + _get_verifier(interaction_object).reset_interactions() + return interaction_object + + +static func _is_mock_or_spy(instance: Object) -> bool: + if instance != null and instance.has_method("__get_verifier"): + return true + + push_error("Error: The given object '%s' is not a mock or spy instance!" % instance) + return false + + +static func _get_verifier(interaction_object: Object) -> GdUnitObjectInteractionsVerifier: + @warning_ignore("unsafe_method_access") + return interaction_object.__get_verifier() diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid new file mode 100644 index 0000000..0639ba5 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid @@ -0,0 +1 @@ +uid://du66nd70kx5wy diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd new file mode 100644 index 0000000..36aa978 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd @@ -0,0 +1,84 @@ +class_name GdUnitObjectInteractionsVerifier + +var expected_interactions: int = -1 +var saved_interactions := Dictionary() +var verified_interactions := Array() + + +func save_function_interaction(func_name: String, args :Array[Variant]) -> void: + var function_args := [func_name] + args + var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for index in saved_interactions.keys().size(): + var key: Variant = saved_interactions.keys()[index] + if matcher.is_match(key): + saved_interactions[key] += 1 + return + saved_interactions[function_args] = 1 + + +func is_verify_interactions() -> bool: + return expected_interactions != -1 + + +func do_verify_interactions(interactions_times: int = 1) -> void: + expected_interactions = interactions_times + + +func verify_interactions(func_name: String, args: Array[Variant]) -> void: + var summary := Dictionary() + var total_interactions := 0 + var function_args := [func_name] + args + var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for index in saved_interactions.keys().size(): + var key: Variant = saved_interactions.keys()[index] + if matcher.is_match(key): + var interactions: int = saved_interactions.get(key, 0) + total_interactions += interactions + summary[key] = interactions + # add as verified + verified_interactions.append(key) + + var assert_tool := GdUnitAssertImpl.new("") + if total_interactions != expected_interactions: + var __expected_summary := {function_args : expected_interactions} + var error_message: String + # if no interactions macht collect not verified interactions for failure report + if summary.is_empty(): + var __current_summary := verify_no_more_interactions() + error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary) + else: + error_message = GdAssertMessages.error_validate_interactions(summary, __expected_summary) + @warning_ignore("return_value_discarded") + assert_tool.report_error(error_message) + else: + @warning_ignore("return_value_discarded") + assert_tool.report_success() + expected_interactions = -1 + + +func verify_no_interactions() -> Dictionary: + var summary := Dictionary() + if not saved_interactions.is_empty(): + for index in saved_interactions.keys().size(): + var func_call: Variant = saved_interactions.keys()[index] + summary[func_call] = saved_interactions[func_call] + return summary + + +func verify_no_more_interactions() -> Dictionary: + var summary := Dictionary() + var called_functions: Array[Variant] = saved_interactions.keys() + if called_functions != verified_interactions: + # collect the not verified functions + var called_but_not_verified := called_functions.duplicate() + for index in verified_interactions.size(): + called_but_not_verified.erase(verified_interactions[index]) + + for index in called_but_not_verified.size(): + var not_verified: Variant = called_but_not_verified[index] + summary[not_verified] = saved_interactions[not_verified] + return summary + + +func reset_interactions() -> void: + saved_interactions.clear() diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid new file mode 100644 index 0000000..a0ea82c --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid @@ -0,0 +1 @@ +uid://c7inpso007x3u diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd new file mode 100644 index 0000000..091241d --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd @@ -0,0 +1,8 @@ +class_name GdUnitSpyFunctionDoubler +extends GdFunctionDoubler + + +func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray: + return GdUnitFunctionDoublerBuilder.new(func_descriptor)\ + .with_verify_block()\ + .build() diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid new file mode 100644 index 0000000..82ba0bd --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://cyunyea578mru diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd new file mode 100644 index 0000000..124d3d4 --- /dev/null +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd @@ -0,0 +1,73 @@ +# This class defines a value extractor by given function name and args +class_name GdUnitFuncValueExtractor +extends GdUnitValueExtractor + +var _func_names :PackedStringArray +var _args :Array + +func _init(func_name :String, p_args :Array) -> void: + _func_names = func_name.split(".") + _args = p_args + + +func func_names() -> PackedStringArray: + return _func_names + + +func args() -> Array: + return _args + + +# Extracts a value by given `func_name` and `args`, +# Allows to use a chained list of functions setarated ba a dot. +# e.g. "func_a.func_b.name" +# do calls instance.func_a().func_b().name() and returns finally the name +# If a function returns an array, all elements will by collected in a array +# e.g. "get_children.get_name" checked a node +# do calls node.get_children() for all childs get_name() and returns all names in an array +# +# if the value not a Object or not accesible be `func_name` the value is converted to `"n.a."` +# expecing null values +func extract_value(value: Variant) -> Variant: + if value == null: + return null + for func_name in func_names(): + if GdArrayTools.is_array_type(value): + var values := Array() + @warning_ignore("unsafe_cast") + for element: Variant in (value as Array): + values.append(_call_func(element, func_name)) + value = values + else: + value = _call_func(value, func_name) + var type := typeof(value) + if type == TYPE_STRING_NAME: + return str(value) + if type == TYPE_STRING and value == "n.a.": + return value + return value + + +func _call_func(value :Variant, func_name :String) -> Variant: + # for array types we need to call explicit by function name, using funcref is only supported for Objects + # TODO extend to all array functions + if GdArrayTools.is_array_type(value) and func_name == "empty": + @warning_ignore("unsafe_cast") + return (value as Array).is_empty() + + if is_instance_valid(value): + # extract from function + var obj_value: Object = value + if obj_value.has_method(func_name): + var extract := Callable(obj_value, func_name) + if extract.is_valid(): + return obj_value.call(func_name) if args().is_empty() else obj_value.callv(func_name, args()) + else: + # if no function exists than try to extract form parmeters + var parameter: Variant = obj_value.get(func_name) + if parameter != null: + return parameter + # nothing found than return 'n.a.' + if GdUnitSettings.is_verbose_assert_warnings(): + push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) + return "n.a." diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid new file mode 100644 index 0000000..b1d4c36 --- /dev/null +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid @@ -0,0 +1 @@ +uid://cnum53k2o000a diff --git a/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd new file mode 100644 index 0000000..906ee2c --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd @@ -0,0 +1,24 @@ +## A fuzzer that generates random boolean values for testing.[br] +## +## This is useful for testing code paths that +## depend on boolean conditions, flags, or toggle states.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## func test_toggle_feature(fuzzer := BoolFuzzer.new(), _fuzzer_iterations = 100): +## var enabled := fuzzer.next_value() +## my_feature.set_enabled(enabled) +## assert_bool(my_feature.is_enabled()),is_equal(enabled) +## [/codeblock] +class_name BoolFuzzer +extends Fuzzer + + +## Generates a random boolean value.[br] +## +## Returns either [code]true[/code] or [code]false[/code] with equal probability. +## This method is called automatically during fuzz testing iterations.[br] +## +## @returns A randomly generated boolean value ([code]true[/code] or [code]false[/code]). +func next_value() -> bool: + return randi() % 2 diff --git a/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid new file mode 100644 index 0000000..9e62317 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/BoolFuzzer.gd.uid @@ -0,0 +1 @@ +uid://dje653g8x2bib diff --git a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd new file mode 100644 index 0000000..5da1a77 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd @@ -0,0 +1,37 @@ +## A fuzzer that generates random floating-point values within a specified range.[br] +## +## This is particularly useful for testing numerical calculations, +## physics simulations, shader parameters, or any code that processes floating-point +## values.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## func test_calculate_damage(fuzzer := FloatFuzzer.new(0.0, 100.0), _fuzzer_iterations := 500): +## var damage := fuzzer.next_value() +## var result = calculate_damage_reduction(damage) +## assert_float(result).is_between(0.0, damage) +## [/codeblock] +## [br] +## [b]Note:[/b] The range is inclusive on both ends, and values are uniformly distributed. +class_name FloatFuzzer +extends Fuzzer + +## Minimum value (inclusive) for generated floats. +var _from: float = 0 +## Maximum value (inclusive) for generated floats. +var _to: float = 0 + +func _init(from: float, to: float) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + + +## Generates a random float value within the configured range.[br] +## +## Returns a uniformly distributed random float between [member _from] and +## [member _to] (inclusive). Each call produces a new random value.[br] +## +## @returns A random float value within the specified range. +func next_value() -> float: + return randf_range(_from, _to) diff --git a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid new file mode 100644 index 0000000..5a3a28b --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid @@ -0,0 +1 @@ +uid://bbf1f8m8l2nrv diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Fuzzer.gd new file mode 100644 index 0000000..e4e597d --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd @@ -0,0 +1,80 @@ +## Base interface for fuzz testing.[br] +## +## Fuzzer is an abstract base class that provides the foundation for creating +## custom fuzzers used in automated testing. Fuzz testing (fuzzing) is a software +## testing technique that involves providing invalid, unexpected, or random data +## as inputs to a program to find bugs and potential security vulnerabilities. +## [br][br] +## To use a fuzzer in your test cases, add optional parameters to your test function: +## [codeblock] +## func test_foo(fuzzer := Fuzzers.randomInt(), _fuzzer_iterations := 10, _fuzzer_seed := 12345): +## var value := fuzzer.next_value() +## # Test logic using the fuzzed value +## [/codeblock] +## [br] +## @tutorial(Fuzzing on Wikipedia): https://en.wikipedia.org/wiki/Fuzzing +@abstract +class_name Fuzzer +extends RefCounted + +## Default number of iterations for fuzz testing when not specified. +const ITERATION_DEFAULT_COUNT := 1000 +## Parameter name for passing the fuzzer instance to test functions. +const ARGUMENT_FUZZER_INSTANCE := "fuzzer" +## Parameter name for specifying the number of iterations in test functions. +const ARGUMENT_ITERATIONS := "fuzzer_iterations" +## Parameter name for specifying the random seed in test functions. +const ARGUMENT_SEED := "fuzzer_seed" + +## Current iteration index during fuzzing execution. +var _iteration_index := 0 +## Maximum number of iterations to run for this fuzzer. +var _iteration_limit := ITERATION_DEFAULT_COUNT + + +## Generates the next fuzz value.[br] +## +## This abstract method must be implemented by derived classes to provide +## the specific fuzzing logic for generating test values.[br] +## +## [b]Example implementation:[/b] +## [codeblock] +## func next_value() -> int: +## return randi_range(0, 100) +## [/codeblock] +## +## @returns The next generated fuzz value. The type depends on the specific fuzzer implementation. +@abstract +func next_value() -> Variant + + +## Returns the current iteration index.[br] +## +## Useful for tracking progress during fuzzing or for debugging purposes +## when a specific iteration causes a failure.[br] +## +## [b]Example:[/b] +## [codeblock] +## if fuzzer.iteration_index() % 100 == 0: +## print("Processed %d iterations" % fuzzer.iteration_index()) +## [/codeblock] +## +## @returns The current iteration index, starting from 0. +func iteration_index() -> int: + return _iteration_index + + +## Returns the maximum number of iterations for this fuzzer.[br] +## +## This value determines how many times the fuzzer will generate values +## during a test run. It can be overridden by the [code]fuzzer_iterations[/code] +## parameter in test functions.[br] +## +## [b]Example:[/b] +## [codeblock] +## print("Running %d fuzzing iterations" % fuzzer.iteration_limit()) +## [/codeblock] +## +## @returns The maximum number of iterations to be executed. +func iteration_limit() -> int: + return _iteration_limit diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid new file mode 100644 index 0000000..4c8fc04 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid @@ -0,0 +1 @@ +uid://dkua1l6tns0da diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd new file mode 100644 index 0000000..4100d51 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd @@ -0,0 +1,77 @@ +## A fuzzer that generates random integer values with optional even/odd constraints.[br] +## +## It supports three modes: normal (any integer), even-only, +## and odd-only generation. This is useful for testing array indices, loop counters, +## enumeration values, or any code that processes integer values.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## # Test with any integer in range +## func test_array_access(fuzzer = IntFuzzer.new(0, 99), fuzzer_iterations = 100): +## var index = fuzzer.next_value() +## var array = create_array(100) +## assert(array[index] != null) +## +## # Test with only even numbers +## func test_even_processing(fuzzer := IntFuzzer.new(0, 100, IntFuzzer.EVEN)): +## var even_num := fuzzer.next_value() +## assert_int(even_num % 2).is_equal(0) +## [/codeblock] +class_name IntFuzzer +extends Fuzzer + + +## Generates any integer within the range. +enum { + NORMAL, ## Generate any integer within the specified range. + EVEN, ## Generate only even integers within the specified range. + ODD ## Generate only odd integers within the specified range. +} + + +## Minimum value (inclusive) for generated integers. +var _from: int = 0 +## Maximum value (inclusive) for generated integers. +var _to: int = 0 +## Generation mode: NORMAL, EVEN, or ODD. +var _mode: int = NORMAL + + +func _init(from: int, to: int, mode: int = NORMAL) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + _mode = mode + + +## Generates a random integer value based on the configured mode.[br] +## +## Returns a random integer between [member _from] and [member _to] (inclusive).[br] +## The value will be constrained according to the [member _mode]:[br] +## - [constant NORMAL]: Any integer in the range[br] +## - [constant EVEN]: Only even integers[br] +## - [constant ODD]: Only odd integers[br] +## +## [b]Example:[/b] +## [codeblock] +## var normal_fuzzer = IntFuzzer.new(1, 10, IntFuzzer.NORMAL) +## var even_fuzzer = IntFuzzer.new(1, 10, IntFuzzer.EVEN) +## var odd_fuzzer = IntFuzzer.new(1, 10, IntFuzzer.ODD) +## +## print(normal_fuzzer.next_value()) # Could be any: 1, 2, 3, ..., 10 +## print(even_fuzzer.next_value()) # Only even: 2, 4, 6, 8, 10 +## print(odd_fuzzer.next_value()) # Only odd: 1, 3, 5, 7, 9 +## [/codeblock] +## +## @returns A random integer value within the specified range and mode +func next_value() -> int: + var value := randi_range(_from, _to) + match _mode: + NORMAL: + return value + EVEN: + return int((value / 2.0) * 2) + ODD: + return int((value / 2.0) * 2 + 1) + _: + return value diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid new file mode 100644 index 0000000..aec8ebb --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid @@ -0,0 +1 @@ +uid://bytoh5b43evyh diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd new file mode 100644 index 0000000..277b4ac --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd @@ -0,0 +1,112 @@ +## A fuzzer that generates random strings with configurable length and character sets.[br] +## +## It supports custom character sets defined by patterns or ranges, +## making it ideal for testing input validation, text processing, parsers, or any +## code that handles string data.[br] +## +## The fuzzer uses a pattern syntax to define allowed characters:[br] +## - Single characters: [code]abc[/code] allows 'a', 'b', 'c'[br] +## - Ranges: [code]a-z[/code] allows lowercase letters[br] +## - Special patterns: [code]\\w[/code] (word chars), [code]\\p{L}[/code] (letters), [code]\\p{N}[/code] (numbers)[br] +## +## [b]Usage example:[/b] +## [codeblock] +## # Test with alphanumeric strings +## func test_username(fuzzer := StringFuzzer.new(3, 20, "a-zA-Z0-9"), _fuzzer_iterations := 100): +## var username _= fuzzer.next_value() +## assert_bool(validate_username(username)).is_true() +## +## # Test with special characters +## func test_password(fuzzer := StringFuzzer.new(8, 32, "a-zA-Z0-9!@#$%"), _fuzzer_iterations := 100) -> void: +## var password := fuzzer.next_value() +## assert_str(password).has_length(8, Comparator.GREATER_EQUAL).has_length(32, Comparator.LESS_EQUAL) +## [/codeblock] +class_name StringFuzzer +extends Fuzzer + +## Default character set pattern including word characters, letters, numbers, and common symbols.[br] +## Includes: word characters (\\w), Unicode letters (\\p{L}), Unicode numbers (\\p{N}), +## and the characters: +, -, _, ' +const DEFAULT_CHARSET = "\\w\\p{L}\\p{N}+-_'" + +## Minimum length for generated strings (inclusive). +var _min_length: int +## Maximum length for generated strings (inclusive). +var _max_length: int +## Array of character codes that can be used in generated strings. +var _charset: PackedInt32Array + + +func _init(min_length: int, max_length: int, pattern: String = DEFAULT_CHARSET) -> void: + _min_length = min_length + _max_length = max_length + 1 # +1 for inclusive + assert(not null or not pattern.is_empty()) + assert(_min_length > 0 and _min_length < _max_length) + _charset = _extract_charset(pattern) + + +## Generates a random string based on configured parameters.[br] +## +## Creates a string with random length between [member _min_length] and +## [member _max_length], using only characters from the configured charset. +## Each character is selected randomly and independently.[br] +## +## [b]Example:[/b] +## [codeblock] +## var fuzzer = StringFuzzer.new(5, 10, "ABC") +## for i in range(5): +## var str = fuzzer.next_value() +## print("Generated: ", str) +## # Possible outputs: "ABCAB", "BCAABCA", "CCCBAA", etc. +## assert(str.length() >= 5 and str.length() <= 10) +## for c in str: +## assert(c in ["A", "B", "C"]) +## [/codeblock] +## +## @returns A random string matching the configured constraints. +func next_value() -> String: + var value := PackedInt32Array() + var max_char := len(_charset) + var length: int = max(_min_length, randi() % _max_length) + for i in length: + @warning_ignore("return_value_discarded") + value.append(_charset[randi() % max_char]) + return value.to_byte_array().get_string_from_utf32() + + +static func _extract_charset(pattern: String) -> PackedInt32Array: + var reg := RegEx.new() + if reg.compile(pattern) != OK: + push_error("Invalid pattern to generate Strings! Use e.g '\\w\\p{L}\\p{N}+-_'") + return PackedInt32Array() + + var charset := PackedInt32Array() + var char_before := -1 + var index := 0 + while index < pattern.length(): + var char_current := pattern.unicode_at(index) + # - range token at first or last pos? + if char_current == 45 and (index == 0 or index == pattern.length()-1): + charset.append(char_current) + index += 1 + continue + index += 1 + # range starts + if char_current == 45 and char_before != -1: + var char_next := pattern.unicode_at(index) + var characters := _build_chars(char_before, char_next) + for character in characters: + charset.append(character) + char_before = -1 + index += 1 + continue + char_before = char_current + charset.append(char_current) + return charset + + +static func _build_chars(from: int, to: int) -> PackedInt32Array: + var characters := PackedInt32Array() + for character in range(from+1, to+1): + characters.append(character) + return characters diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid new file mode 100644 index 0000000..b2cc6f3 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid @@ -0,0 +1 @@ +uid://cvricml671ix diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd new file mode 100644 index 0000000..70ef31b --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd @@ -0,0 +1,45 @@ +## A fuzzer that generates random Vector2 values within a specified rectangular range.[br] +## +## This is particularly useful for testing 2D physics, movement +## systems, UI positioning, sprite coordinates, or any code that processes 2D vectors.[br] +## +## The fuzzer generates vectors where each component (x, y) is independently randomized +## within its respective range, creating a uniform distribution over the rectangular area.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## # Test 2D movement within screen bounds +## func test_movement(fuzzer := Vector2Fuzzer.new(Vector2.ZERO, Vector2(1920, 1080)), _fuzzer_iterations := 200) -> void: +## var position := fuzzer.next_value() +## player.set_position(position) +## +## [/codeblock] +class_name Vector2Fuzzer +extends Fuzzer + + +## Minimum bounds for the generated vectors (inclusive for both x and y). +var _from: Vector2 +## Maximum bounds for the generated vectors (inclusive for both x and y). +var _to: Vector2 + + +func _init(from: Vector2, to: Vector2) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + + +## Generates a random Vector2 within the configured rectangular range.[br] +## +## Returns a Vector2 where each component is independently randomized:[br] +## - x: random float between [code]_from.x[/code] and [code]_to.x[/code][br] +## - y: random float between [code]_from.y[/code] and [code]_to.y[/code][br] +## +## The distribution is uniform over the rectangular area defined by the bounds.[br] +## +## @returns A random Vector2 within the specified range. +func next_value() -> Vector2: + var x := randf_range(_from.x, _to.x) + var y := randf_range(_from.y, _to.y) + return Vector2(x, y) diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid new file mode 100644 index 0000000..ace72e4 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid @@ -0,0 +1 @@ +uid://dq8k7mmuct2my diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd new file mode 100644 index 0000000..b54870d --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd @@ -0,0 +1,48 @@ +## A fuzzer that generates random Vector3 values within a specified box range.[br] +## +## This is particularly useful for testing 3D physics, spatial +## positioning, camera systems, particle effects, or any code that processes 3D vectors.[br] +## +## The fuzzer generates vectors where each component (x, y, z) is independently +## randomized within its respective range, creating a uniform distribution over the +## 3D box volume.[br] +## +## [b]Usage example:[/b] +## [codeblock] +## # Test 3D object placement within world bounds +## func test_spawn_position(fuzzer := Vector3Fuzzer.new(Vector3(-100, 0, -100), Vector3(100, 50, 100)), _fuzzer_iterations := 300): +## var position := fuzzer.next_value() +## var object = spawn_object(position) +## +## [/codeblock] +class_name Vector3Fuzzer +extends Fuzzer + + +## Minimum bounds for the generated vectors (inclusive for x, y, and z). +var _from: Vector3 +## Maximum bounds for the generated vectors (inclusive for x, y, and z). +var _to: Vector3 + + +func _init(from: Vector3, to: Vector3) -> void: + assert(from <= to, "Invalid range!") + _from = from + _to = to + + +## Generates a random Vector3 within the configured box range.[br] +## +## Returns a Vector3 where each component is independently randomized:[br] +## - x: random float between [code]_from.x[/code] and [code]_to.x[/code][br] +## - y: random float between [code]_from.y[/code] and [code]_to.y[/code][br] +## - z: random float between [code]_from.z[/code] and [code]_to.z[/code][br] +## +## The distribution is uniform over the 3D box volume defined by the bounds.[br] +## +## @returns A random Vector3 within the specified range. +func next_value() -> Vector3: + var x := randf_range(_from.x, _to.x) + var y := randf_range(_from.y, _to.y) + var z := randf_range(_from.z, _to.z) + return Vector3(x, y, z) diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid new file mode 100644 index 0000000..5e87e2d --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid @@ -0,0 +1 @@ +uid://crfcm4avgcrwy diff --git a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd new file mode 100644 index 0000000..bd50313 --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd @@ -0,0 +1,11 @@ +class_name AnyArgumentMatcher +extends GdUnitArgumentMatcher + + +@warning_ignore("unused_parameter") +func is_match(value :Variant) -> bool: + return true + + +func _to_string() -> String: + return "any()" diff --git a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid new file mode 100644 index 0000000..8421ec6 --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://c3hrtgy2xnchj diff --git a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd new file mode 100644 index 0000000..ba34431 --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd @@ -0,0 +1,50 @@ +class_name AnyBuildInTypeArgumentMatcher +extends GdUnitArgumentMatcher + +var _type : PackedInt32Array = [] + + +func _init(type :PackedInt32Array) -> void: + _type = type + + +func is_match(value :Variant) -> bool: + return _type.has(typeof(value)) + + +func _to_string() -> String: + match _type[0]: + TYPE_BOOL: return "any_bool()" + TYPE_STRING, TYPE_STRING_NAME: return "any_string()" + TYPE_INT: return "any_int()" + TYPE_FLOAT: return "any_float()" + TYPE_COLOR: return "any_color()" + TYPE_VECTOR2: return "any_vector2()" if _type.size() == 1 else "any_vector()" + TYPE_VECTOR2I: return "any_vector2i()" + TYPE_VECTOR3: return "any_vector3()" + TYPE_VECTOR3I: return "any_vector3i()" + TYPE_VECTOR4: return "any_vector4()" + TYPE_VECTOR4I: return "any_vector4i()" + TYPE_RECT2: return "any_rect2()" + TYPE_RECT2I: return "any_rect2i()" + TYPE_PLANE: return "any_plane()" + TYPE_QUATERNION: return "any_quat()" + TYPE_AABB: return "any_aabb()" + TYPE_BASIS: return "any_basis()" + TYPE_TRANSFORM2D: return "any_transform_2d()" + TYPE_TRANSFORM3D: return "any_transform_3d()" + TYPE_NODE_PATH: return "any_node_path()" + TYPE_RID: return "any_rid()" + TYPE_OBJECT: return "any_object()" + TYPE_DICTIONARY: return "any_dictionary()" + TYPE_ARRAY: return "any_array()" + TYPE_PACKED_BYTE_ARRAY: return "any_packed_byte_array()" + TYPE_PACKED_INT32_ARRAY: return "any_packed_int32_array()" + TYPE_PACKED_INT64_ARRAY: return "any_packed_int64_array()" + TYPE_PACKED_FLOAT32_ARRAY: return "any_packed_float32_array()" + TYPE_PACKED_FLOAT64_ARRAY: return "any_packed_float64_array()" + TYPE_PACKED_STRING_ARRAY: return "any_packed_string_array()" + TYPE_PACKED_VECTOR2_ARRAY: return "any_packed_vector2_array()" + TYPE_PACKED_VECTOR3_ARRAY: return "any_packed_vector3_array()" + TYPE_PACKED_COLOR_ARRAY: return "any_packed_color_array()" + _: return "any()" diff --git a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid new file mode 100644 index 0000000..d813f6c --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://c8ci3s3xppdsv diff --git a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd new file mode 100644 index 0000000..b5e3de3 --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd @@ -0,0 +1,32 @@ +class_name AnyClazzArgumentMatcher +extends GdUnitArgumentMatcher + +var _clazz :Object + + +func _init(clazz :Object) -> void: + _clazz = clazz + + +func is_match(value :Variant) -> bool: + if typeof(value) != TYPE_OBJECT: + return false + if is_instance_valid(value) and GdObjects.is_script(_clazz): + @warning_ignore("unsafe_cast") + return (value as Object).get_script() == _clazz + return is_instance_of(value, _clazz) + + +func _to_string() -> String: + if (_clazz as Object).is_class("GDScriptNativeClass"): + @warning_ignore("unsafe_method_access") + var instance :Object = _clazz.new() + var clazz_name := instance.get_class() + if not instance is RefCounted: + instance.free() + return "any_class(<"+clazz_name+">)"; + if _clazz is GDScript: + var result := GdObjects.extract_class_name(_clazz) + if result.is_success(): + return "any_class(<"+ result.value() + ">)" + return "any_class()" diff --git a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid new file mode 100644 index 0000000..f840def --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://c3rfm5hfnbvxs diff --git a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd new file mode 100644 index 0000000..f779bd7 --- /dev/null +++ b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd @@ -0,0 +1,22 @@ +class_name ChainedArgumentMatcher +extends GdUnitArgumentMatcher + +var _matchers :Array + + +func _init(matchers :Array) -> void: + _matchers = matchers + + +func is_match(arguments :Variant) -> bool: + var arg_array: Array = arguments + if arg_array == null or arg_array.size() != _matchers.size(): + return false + + for index in arg_array.size(): + var arg: Variant = arg_array[index] + var matcher: GdUnitArgumentMatcher = _matchers[index] + + if not matcher.is_match(arg): + return false + return true diff --git a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid new file mode 100644 index 0000000..f8908b8 --- /dev/null +++ b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://dfgcw2dj1ou2j diff --git a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd new file mode 100644 index 0000000..2d387ed --- /dev/null +++ b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd @@ -0,0 +1,22 @@ +class_name EqualsArgumentMatcher +extends GdUnitArgumentMatcher + +var _current :Variant +var _auto_deep_check_mode :bool + + +func _init(current :Variant, auto_deep_check_mode := false) -> void: + _current = current + _auto_deep_check_mode = auto_deep_check_mode + + +func is_match(value :Variant) -> bool: + var case_sensitive_check := true + return GdObjects.equals(_current, value, case_sensitive_check, compare_mode(value)) + + +func compare_mode(value :Variant) -> GdObjects.COMPARE_MODE: + if _auto_deep_check_mode and is_instance_valid(value): + # we do deep check on all InputEvent's + return GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST if value is InputEvent else GdObjects.COMPARE_MODE.OBJECT_REFERENCE + return GdObjects.COMPARE_MODE.OBJECT_REFERENCE diff --git a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid new file mode 100644 index 0000000..45be61e --- /dev/null +++ b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://b50351ujny1kj diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd new file mode 100644 index 0000000..d5adc4b --- /dev/null +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd @@ -0,0 +1,13 @@ +## The base class of all argument matchers +class_name GdUnitArgumentMatcher +extends RefCounted + + +@warning_ignore("unused_parameter") +func is_match(value: Variant) -> bool: + return true + + +func _to_string() -> String: + assert(false, "`_to_string()` Is not implemented!") + return "" diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid new file mode 100644 index 0000000..72ce2f1 --- /dev/null +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid @@ -0,0 +1 @@ +uid://dn5i0ocgyf3pc diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd new file mode 100644 index 0000000..6a70cc1 --- /dev/null +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd @@ -0,0 +1,42 @@ +class_name GdUnitArgumentMatchers +extends RefCounted + +const TYPE_ANY = TYPE_MAX + 100 + + +static func to_matcher(arguments: Array[Variant], auto_deep_check_mode := false) -> ChainedArgumentMatcher: + var matchers: Array[Variant] = [] + for arg: Variant in arguments: + # argument is already a matcher + if arg is GdUnitArgumentMatcher: + matchers.append(arg) + else: + # pass argument into equals matcher + matchers.append(EqualsArgumentMatcher.new(arg, auto_deep_check_mode)) + return ChainedArgumentMatcher.new(matchers) + + +static func any() -> GdUnitArgumentMatcher: + return AnyArgumentMatcher.new() + + +static func by_type(type: int) -> GdUnitArgumentMatcher: + return AnyBuildInTypeArgumentMatcher.new([type]) + + +static func by_types(types: PackedInt32Array) -> GdUnitArgumentMatcher: + return AnyBuildInTypeArgumentMatcher.new(types) + + +static func any_class(clazz: Object) -> GdUnitArgumentMatcher: + return AnyClazzArgumentMatcher.new(clazz) + + +static func is_variant_string_matching(value: Variant) -> GdUnitResult: + if value is String or value is StringName: + return GdUnitResult.success() + if value is GdUnitArgumentMatcher: + if str(value) == "any()" or str(value) == "any_string()": + return GdUnitResult.success() + return GdUnitResult.error("Only 'any()' and 'any_string()' argument matchers are allowed!") + return GdUnitResult.error("Only String or StringName types are allowed!") diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid new file mode 100644 index 0000000..fb1e857 --- /dev/null +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid @@ -0,0 +1 @@ +uid://bvppkmrwgddwe diff --git a/addons/gdUnit4/src/mocking/GdUnitMock.gd b/addons/gdUnit4/src/mocking/GdUnitMock.gd new file mode 100644 index 0000000..c520d92 --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMock.gd @@ -0,0 +1,45 @@ +class_name GdUnitMock +extends RefCounted + +## do call the real implementation +const CALL_REAL_FUNC = "CALL_REAL_FUNC" +## do return a default value for primitive types or null +const RETURN_DEFAULTS = "RETURN_DEFAULTS" +## 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 = "RETURN_DEEP_STUB" + +var _value: Variant + + +func _init(value: Variant) -> void: + _value = value + + +## Selects the mock to work on, used in combination with [method GdUnitTestSuite.do_return][br] +## Example: +## [codeblock] +## do_return(false).on(myMock).is_selected() +## [/codeblock] +func on(obj: Variant) -> Variant: + if not GdUnitMock._is_mock_or_spy(obj, "__do_return"): + return obj + @warning_ignore("unsafe_method_access") + return obj.__do_return(_value) + + +## [color=yellow]`checked` is obsolete, use `on` instead [/color] +func checked(obj :Object) -> Object: + push_warning("Using a deprecated function 'checked' use `on` instead") + return on(obj) + + +static func _is_mock_or_spy(obj: Variant, func_sig: String) -> bool: + if obj is Object and not as_object(obj).has_method(func_sig): + push_error("Error: You try to use a non mock or spy!") + return false + return true + + +static func as_object(value: Variant) -> Object: + return value diff --git a/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid new file mode 100644 index 0000000..061e566 --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid @@ -0,0 +1 @@ +uid://b48r8sgf1xtsk diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd new file mode 100644 index 0000000..537e923 --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd @@ -0,0 +1,186 @@ +class_name GdUnitMockBuilder +extends GdUnitClassDoubler + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const MOCK_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/mocking/GdUnitMockImpl.gd") + + +static func is_push_errors() -> bool: + return GdUnitSettings.is_report_push_errors() + + +static func build(clazz :Variant, mock_mode :String, debug_write := false) -> Variant: + var push_errors := is_push_errors() + if not is_mockable(clazz, push_errors): + return null + # mocking a scene? + if GdObjects.is_scene(clazz): + var packed_scene: PackedScene = clazz + return mock_on_scene(packed_scene, debug_write) + elif typeof(clazz) == TYPE_STRING and str(clazz).ends_with(".tscn"): + var packed_scene: PackedScene = load(str(clazz)) + return mock_on_scene(packed_scene, debug_write) + # mocking a script + var instance := create_instance(clazz) + if instance == null: + push_error("Can't create instance of class %s" % clazz) + var mock := mock_on_script(instance, clazz, [ "get_script"], debug_write) + if not instance is RefCounted: + instance.free() + if mock == null: + return null + var mock_instance: Object = mock.new() + @warning_ignore("unsafe_method_access") + mock_instance.__init(mock, mock_mode) + return register_auto_free(mock_instance) + + +static func create_instance(clazz: Variant) -> Object: + match typeof(clazz): + TYPE_OBJECT: + var obj: Object = clazz + if clazz is GDScript: + var script: GDScript = clazz + var args := GdObjects.build_function_default_arguments(script, "_init") + return script.callv("new", args) + elif obj.is_class("GDScriptNativeClass"): + @warning_ignore("unsafe_method_access") + return obj.new() + TYPE_STRING: + var clazz_name: String = clazz + if clazz_name.ends_with(".gd"): + var script: GDScript = load(clazz_name) + var args := GdObjects.build_function_default_arguments(script, "_init") + return script.callv("new", args) + elif ClassDB.can_instantiate(clazz_name): + return ClassDB.instantiate(clazz_name) + + push_error("Can't create a mock validation instance from class: `%s`" % clazz) + return null + + +static func mock_on_scene(scene: PackedScene, debug_write: bool) -> Variant: + var push_errors := is_push_errors() + if not scene.can_instantiate(): + if push_errors: + push_error("Can't instanciate scene '%s'" % scene.resource_path) + return null + var scene_instance := scene.instantiate() + # we can only mock checked a scene with attached script + var scene_script: Script = scene_instance.get_script() + if scene_script == null: + if push_errors: + push_error("Can't create a mockable instance for a scene without script '%s'" % scene.resource_path) + @warning_ignore("return_value_discarded") + GdUnitTools.free_instance(scene_instance) + return null + + var script_path := scene_script.get_path() + var mock := mock_on_script(scene_instance, script_path, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write) + if mock == null: + return null + scene_instance.set_script(mock) + @warning_ignore("unsafe_method_access") + scene_instance.__init(mock, GdUnitMock.CALL_REAL_FUNC) + return register_auto_free(scene_instance) + + +static func get_class_info(clazz :Variant) -> Dictionary: + var clazz_name :String = GdObjects.extract_class_name(clazz).value() + var clazz_path := GdObjects.extract_class_path(clazz) + return { + "class_name" : clazz_name, + "class_path" : clazz_path + } + + +static func mock_on_script(instance :Object, clazz :Variant, function_excludes :PackedStringArray, debug_write :bool) -> GDScript: + var function_doubler := GdUnitMockFunctionDoubler.new() + var class_info := get_class_info(clazz) + var clazz_name :String = class_info.get("class_name") + var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) + var mock_template := MOCK_TEMPLATE.source_code.format({ + "instance_id" : abs(instance.get_instance_id()), + "gdunit_source_class": clazz_name if clazz_path.is_empty() else clazz_path[0] + }) + var lines := load_template(mock_template, class_info) + lines += double_functions(instance, clazz_name, clazz_path, function_doubler, function_excludes) + # We disable warning/errors for inferred_declaration + if Engine.get_version_info().hex >= 0x40400: + lines.insert(0, '@warning_ignore_start("inferred_declaration")') + lines.append('@warning_ignore_restore("inferred_declaration")') + + var mock := GDScript.new() + mock.source_code = "\n".join(lines) + mock.resource_name = "Mock%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] + mock.resource_path = "%s/%s" % [GdUnitFileAccess.create_temp_dir("mock"), mock.resource_name] + + if debug_write: + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(mock.resource_path) + @warning_ignore("return_value_discarded") + ResourceSaver.save(mock, mock.resource_path) + var error := mock.reload(true) + if error != OK: + push_error("Critical!!!, MockBuilder error, please contact the developer.") + return null + return mock + + +static func is_mockable(clazz :Variant, push_errors :bool=false) -> bool: + var clazz_type := typeof(clazz) + if clazz_type != TYPE_OBJECT and clazz_type != TYPE_STRING: + push_error("Invalid clazz type is used") + return false + # is PackedScene + if GdObjects.is_scene(clazz): + return true + if GdObjects.is_native_class(clazz): + return true + # verify class type + if GdObjects.is_object(clazz): + if GdObjects.is_instance(clazz): + if push_errors: + push_error("It is not allowed to mock an instance '%s', use class name instead, Read 'Mocker' documentation for details" % clazz) + return false + + if not GdObjects.can_be_instantiate(clazz): + if push_errors: + push_error("Can't create a mockable instance for class '%s'" % clazz) + return false + return true + # verify by class name checked registered classes + var clazz_name: String = clazz + if ClassDB.class_exists(clazz_name): + if Engine.has_singleton(clazz_name): + if push_errors: + push_error("Mocking a singelton class '%s' is not allowed! Read 'Mocker' documentation for details" % clazz_name) + return false + if not ClassDB.can_instantiate(clazz_name): + if push_errors: + push_error("Mocking class '%s' is not allowed it cannot be instantiated!" % clazz_name) + return false + # exclude classes where name starts with a underscore + if clazz_name.find("_") == 0: + if push_errors: + push_error("Can't create a mockable instance for protected class '%s'" % clazz_name) + return false + return true + # at least try to load as a script + var clazz_path := clazz_name + if not FileAccess.file_exists(clazz_path): + if push_errors: + push_error("'%s' cannot be mocked for the specified resource path, the resource does not exist" % clazz_name) + return false + # finally verify is a script resource + var resource := load(clazz_path) + if resource == null: + if push_errors: + push_error("'%s' cannot be mocked the script cannot be loaded." % clazz_name) + return false + # finally check is extending from script + return GdObjects.is_script(resource) or GdObjects.is_scene(resource) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid new file mode 100644 index 0000000..f19798e --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid @@ -0,0 +1 @@ +uid://c6dx1icbg1mer diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd new file mode 100644 index 0000000..774ca3e --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd @@ -0,0 +1,143 @@ +class_name DoubledMockClassSourceClassName + +################################################################################ +# internal mocking stuff +################################################################################ + +const __INSTANCE_ID := "gdunit_doubler_instance_id_{instance_id}" + + +class GdUnitMockDoublerState: + const __SOURCE_CLASS := "{gdunit_source_class}" + + var excluded_methods := PackedStringArray() + var working_mode := GdUnitMock.RETURN_DEFAULTS + var is_prepare_return := false + var return_values := Dictionary() + var return_value: Variant = null + + + func _init(working_mode_ := GdUnitMock.RETURN_DEFAULTS) -> void: + working_mode = working_mode_ + + +var __mock_state := GdUnitMockDoublerState.new() +@warning_ignore("unused_private_class_variable") +var __verifier_instance := GdUnitObjectInteractionsVerifier.new() + + +func __init(__script: GDScript, mock_working_mode: String) -> void: + super.set_script(__script) + __init_doubler() + __mock_state.working_mode = mock_working_mode + + +static func __doubler_state() -> GdUnitMockDoublerState: + if Engine.has_meta(__INSTANCE_ID): + return Engine.get_meta(__INSTANCE_ID).__mock_state + return null + + +func __init_doubler() -> void: + Engine.set_meta(__INSTANCE_ID, self) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE and Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) + + +static func __get_verifier() -> GdUnitObjectInteractionsVerifier: + return Engine.get_meta(__INSTANCE_ID).__verifier_instance + + +static func __is_prepare_return_value() -> bool: + return __doubler_state().is_prepare_return + + +static func __sort_by_argument_matcher(__left_args: Array, __right_args: Array) -> bool: + for __index in __left_args.size(): + var __larg: Variant = __left_args[__index] + if __larg is GdUnitArgumentMatcher: + return false + return true + + +# we need to sort by matcher arguments so that they are all at the end of the list +static func __sort_dictionary(__unsorted_args: Dictionary) -> Dictionary: + # only need to sort if contains more than one entry + if __unsorted_args.size() <= 1: + return __unsorted_args + var __sorted_args: Array = __unsorted_args.keys() + __sorted_args.sort_custom(__sort_by_argument_matcher) + var __sorted_result := {} + for __index in __sorted_args.size(): + var key :Variant = __sorted_args[__index] + __sorted_result[key] = __unsorted_args[key] + return __sorted_result + + +static func __save_function_return_value(__func_name: String, __func_args: Array) -> void: + var doubler_state := __doubler_state() + var mocked_return_value_by_args: Dictionary = doubler_state.return_values.get(__func_name, {}) + + mocked_return_value_by_args[__func_args] = doubler_state.return_value + doubler_state.return_values[__func_name] = __sort_dictionary(mocked_return_value_by_args) + doubler_state.return_value = null + doubler_state.is_prepare_return = false + + +static func __is_mocked_args_match(__func_args: Array, __mocked_args: Array) -> bool: + var __is_matching := false + for __index in __mocked_args.size(): + var __fuction_args: Array = __mocked_args[__index] + if __func_args.size() != __fuction_args.size(): + continue + __is_matching = true + for __arg_index in __func_args.size(): + var __func_arg: Variant = __func_args[__arg_index] + var __mock_arg: Variant = __fuction_args[__arg_index] + if __mock_arg is GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") + __is_matching = __is_matching and __mock_arg.is_match(__func_arg) + else: + __is_matching = __is_matching and typeof(__func_arg) == typeof(__mock_arg) and __func_arg == __mock_arg + if not __is_matching: + break + if __is_matching: + break + return __is_matching + + +static func __return_mock_value(__func_name: String, __func_args: Array, __default_return_value: Variant) -> Variant: + var doubler_state := __doubler_state() + if not doubler_state.return_values.has(__func_name): + return __default_return_value + @warning_ignore("unsafe_method_access") + var __mocked_args: Array = doubler_state.return_values.get(__func_name).keys() + for __index in __mocked_args.size(): + var __margs: Variant = __mocked_args[__index] + if __is_mocked_args_match(__func_args, [__margs]): + return doubler_state.return_values[__func_name][__margs] + return __default_return_value + + +static func __is_do_not_call_real_func(__func_name: String, __func_args := []) -> bool: + var doubler_state := __doubler_state() + var __is_call_real_func: bool = doubler_state.working_mode == GdUnitMock.CALL_REAL_FUNC and not doubler_state.excluded_methods.has(__func_name) + # do not call real funcions for mocked functions + if __is_call_real_func and doubler_state.return_values.has(__func_name): + @warning_ignore("unsafe_method_access") + var __mocked_args: Array = doubler_state.return_values.get(__func_name).keys() + return __is_mocked_args_match(__func_args, __mocked_args) + return !__is_call_real_func + + +func __exclude_method_call(exluded_methods: PackedStringArray) -> void: + __doubler_state().excluded_methods.append_array(exluded_methods) + + +func __do_return(mock_do_return_value: Variant) -> Object: + __doubler_state().return_value = mock_do_return_value + __doubler_state().is_prepare_return = true + return self diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid new file mode 100644 index 0000000..fdbe945 --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid @@ -0,0 +1 @@ +uid://c73tijwmx847i diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd new file mode 100644 index 0000000..2fc39be --- /dev/null +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd @@ -0,0 +1,41 @@ +extends RefCounted +class_name ErrorLogEntry + + +enum TYPE { + SCRIPT_ERROR, + PUSH_ERROR, + PUSH_WARNING +} + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +var _type: TYPE +var _line: int +var _message: String +var _stack_trace: GdUnitStackTrace + + +func _init(type: TYPE, line: int, message: String, stack_trace: GdUnitStackTrace) -> void: + _type = type + _line = line + _message = message + _stack_trace = stack_trace + + +func _to_string() -> String: + return _message + + +static func of_push_warning(line: int, message: String, stack_trace: GdUnitStackTrace) -> ErrorLogEntry: + return ErrorLogEntry.new(TYPE.PUSH_WARNING, line, message, stack_trace) + + +static func of_push_error(line: int, message: String, stack_trace: GdUnitStackTrace) -> ErrorLogEntry: + return ErrorLogEntry.new(TYPE.PUSH_ERROR, line, message, stack_trace) + + +static func of_script_error(line: int, message: String, stack_trace: GdUnitStackTrace) -> ErrorLogEntry: + return ErrorLogEntry.new(TYPE.SCRIPT_ERROR, line, message, stack_trace) diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid new file mode 100644 index 0000000..ab6062d --- /dev/null +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid @@ -0,0 +1 @@ +uid://dq7dy5wkfu4s diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd new file mode 100644 index 0000000..b6429ca --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd @@ -0,0 +1,24 @@ +# GdUnit Monitoring Base Class +class_name GdUnitMonitor +extends RefCounted + +var _id :String + +# constructs new Monitor with given id +func _init(p_id :String) -> void: + _id = p_id + + +# Returns the id of the monitor to uniqe identify +func id() -> String: + return _id + + +# starts monitoring +func start() -> void: + pass + + +# stops monitoring +func stop() -> void: + pass diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid new file mode 100644 index 0000000..997e6b7 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid @@ -0,0 +1 @@ +uid://ddeije333kepf diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd new file mode 100644 index 0000000..f8207db --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd @@ -0,0 +1,13 @@ +class_name GdUnitOrphanNodeInfo +extends RefCounted + + +var _id: int +var _type: String +var _stack_element: GdUnitStackTraceElement + + +func _init(id: int, type: String, stack_element: GdUnitStackTraceElement) -> void: + _id = id + _type = type + _stack_element = stack_element diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd.uid b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd.uid new file mode 100644 index 0000000..460ad7b --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd.uid @@ -0,0 +1 @@ +uid://dr5bo63bno7ao diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd new file mode 100644 index 0000000..29f482a --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd @@ -0,0 +1,232 @@ +class_name GdUnitOrphanNodesMonitor +extends GdUnitMonitor + + +var _child_monitors: Array[GdUnitOrphanNodesMonitor] = [] +var _orphan_detection_enabled :bool +var _initial_orphans: Array[int] = [] +var _orphan_ids_at_start: Array[int] = [] +var _orphan_ids_at_stop: Array[int] = [] +var _collected_orphan_infos: Array[GdUnitOrphanNodeInfo] = [] + + +func _init(name: String) -> void: + super("OrphanNodesMonitor:" + name) + _orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + _initial_orphans = _get_orphan_node_ids() + + +func add_child_monitor(monitor: GdUnitOrphanNodesMonitor) -> void: + if not _orphan_detection_enabled: + return + _child_monitors.append(monitor) + + +func start() -> void: + if not _orphan_detection_enabled: + return + _collected_orphan_infos.clear() + # Collect current orphan id's to be filtered out at `stop` + _orphan_ids_at_start = _get_orphan_node_ids() + + +func stop() -> void: + if not _orphan_detection_enabled: + return + # Collect only new detected orphan id's, we want only to collect orphans between start and stop time + _orphan_ids_at_stop = _get_orphan_node_ids().filter(func(element: int) -> bool: + # Excluding sub monitores orphans + if _collect_child_orphan_ids().has(element): + return false + # Excluding orphans at start + return not _orphan_ids_at_start.has(element) and not _initial_orphans.has(element) + ) + + +func _collect_child_orphan_ids() -> Array[int]: + var collected_ids: Array[int] = [] + for child_monitor in _child_monitors: + collected_ids.append_array(child_monitor._orphan_ids_at_stop) + collected_ids.append_array(child_monitor._collect_child_orphan_ids()) + return collected_ids + + +func detected_orphans() -> Array[GdUnitOrphanNodeInfo]: + if not _orphan_detection_enabled: + return [] + return _collected_orphan_infos.filter(func(info: GdUnitOrphanNodeInfo) -> bool: + return info._id in _orphan_ids_at_stop + ) + + +func orphans_count() -> int: + if not _orphan_detection_enabled: + return 0 + return _orphan_ids_at_stop.size() + + +func collect() -> void: + if not _orphan_detection_enabled: + return + + stop() + if _orphan_ids_at_stop.is_empty(): + return + + var script_backtraces := Engine.capture_script_backtraces(true) + for orphan_id in _orphan_ids_at_stop: + var orphan_node := instance_from_id(orphan_id) + _collect_orphan_info(orphan_node, script_backtraces) + + +func _collect_orphan_info(orphan_node: Object, script_backtraces: Array[ScriptBacktrace]) -> void: + if orphan_node == null: + return + + var orphan_info := _find_orphan_on_backtraces(orphan_node, script_backtraces) + if orphan_info: + _collected_orphan_infos.append(orphan_info) + return + + if Engine.has_meta("GdUnitSceneRunner"): + var current_scene_runner:GdUnitSceneRunner = Engine.get_meta("GdUnitSceneRunner") + if is_instance_valid(current_scene_runner): + orphan_info = _find_orphan_at_node(orphan_node, current_scene_runner.scene()) + if orphan_info: + _collected_orphan_infos.append(orphan_info) + return + + _collected_orphan_infos.append( + GdUnitOrphanNodeInfo.new( + orphan_node.get_instance_id(), + orphan_node.get_class(), + null) + ) + + +func _find_orphan_at_node(orphan_node: Object, node: Node) -> GdUnitOrphanNodeInfo: + var script: Script = node.get_script() + if script is not GDScript: + return null + + # First search over all properties + for property in script.get_script_property_list(): + # We lookup only over user script variables + var property_usage: int = property["usage"] + if property_usage != PROPERTY_USAGE_SCRIPT_VARIABLE: + continue + + var property_type: int = property["type"] + # Is untyped or type object + if property_type in [TYPE_NIL, TYPE_OBJECT]: + var property_name: String = property["name"] + var property_instance: Variant = node.get(property_name) + @warning_ignore("unsafe_cast") + var property_as_node := property_instance as Node + if property_as_node == null: + continue + # If node match the curren property object + if property_as_node == orphan_node: + var property_class: String = property["class_name"] + var source_line := _find_line_for_property(script, "", property_name) + return GdUnitOrphanNodeInfo.new( + orphan_node.get_instance_id(), + property_class, + GdUnitStackTraceElement.new( + script.resource_path, + source_line, + property_name) + ) + + # Otherwise we need to search on child node script properties + var orphan_info := _find_orphan_at_node(orphan_node, property_as_node) + if orphan_info: + return orphan_info + + # Second over all children + for child_node in node.get_children(): + var orphan_info := _find_orphan_at_node(orphan_node, child_node) + if orphan_info: + return orphan_info + return null + + +func _find_orphan_on_backtraces(orphan_node: Object, script_backtraces: Array[ScriptBacktrace]) -> GdUnitOrphanNodeInfo: + for script_backtrace in script_backtraces: + for frame in script_backtrace.get_frame_count(): + var frame_file := script_backtrace.get_frame_file(frame) + if GdUnitStackTrace.filter_sources(frame_file): + continue + + # Scan function variables + for l_index in script_backtrace.get_local_variable_count(frame): + var variable: Variant = script_backtrace.get_local_variable_value(frame, l_index) + if typeof(variable) in [TYPE_NIL, TYPE_OBJECT]: + @warning_ignore("unsafe_cast") + var node := variable as Node + if node == null: + continue + if variable == orphan_node: + var variable_name := script_backtrace.get_local_variable_name(frame, l_index) + var source_script := script_backtrace.get_frame_file(frame) + var source_function := script_backtrace.get_frame_function(frame) + var script: Script = load(source_script) + var source_line := _find_line_for_property(script, source_function, variable_name) + return GdUnitOrphanNodeInfo.new( + orphan_node.get_instance_id(), + orphan_node.get_class(), + GdUnitStackTraceElement.new(source_script, source_line, variable_name) + ) + else: + var orphan_info := _find_orphan_at_node(orphan_node, node) + if orphan_info: + return orphan_info + + # Scan class members + for m_index in script_backtrace.get_member_variable_count(frame): + var member: Variant = script_backtrace.get_member_variable_value(frame, m_index) + if typeof(member) in [TYPE_NIL, TYPE_OBJECT]: + @warning_ignore("unsafe_cast") + var node := member as Node + if node == null: + continue + if member == orphan_node: + var member_name := script_backtrace.get_member_variable_name(frame, m_index) + return GdUnitOrphanNodeInfo.new( + orphan_node.get_instance_id(), + orphan_node.get_class(), + GdUnitStackTraceElement.new( + script_backtrace.get_frame_file(frame), + script_backtrace.get_frame_line(frame), + member_name)) + else: + var orphan_info := _find_orphan_at_node(orphan_node, node) + if orphan_info: + return orphan_info + return null + + +func _find_line_for_property(script: Script, func_name: String, property_name: String) -> int: + if script == null or not script.has_source_code(): + return -1 + var lines := script.get_source_code().split("\n") + var func_start_index := 0 + for index in range(0, lines.size()): + var line := lines[index] + if not func_name.is_empty(): + if line.begins_with("func") and line.contains(func_name): + func_start_index = index + 1 + break; + + for index in range(func_start_index, lines.size()): + var line := lines[index] + if line.contains(property_name): + return index + 1 + if line.begins_with("func"): + break + return -1 + + +static func _get_orphan_node_ids() -> Array[int]: + @warning_ignore("unsafe_property_access", "unsafe_method_access") + return Engine.get_main_loop().root.get_orphan_node_ids() diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid new file mode 100644 index 0000000..abf51e2 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid @@ -0,0 +1 @@ +uid://tce2d2g6kbnd diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd new file mode 100644 index 0000000..24042c6 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd @@ -0,0 +1,113 @@ +class_name GodotGdErrorMonitor +extends GdUnitMonitor + + +var _logger: GdUnitLogger + + +class GdUnitLogger extends Logger: + var _entries: Array[ErrorLogEntry] = [] + var _is_report_push_errors: bool + var _is_report_script_errors: bool + + + func _init(is_report_push_errors: bool, is_report_script_errors: bool) -> void: + _is_report_push_errors = is_report_push_errors + _is_report_script_errors = is_report_script_errors + OS.add_logger(self) + + + func entries() -> Array[ErrorLogEntry]: + return _entries + + func erase_log_entry(log_entry: ErrorLogEntry) -> void: + for entry in _entries: + if entry._type == log_entry._type and entry._message == log_entry._message: + _entries.erase(entry) + return + + + func _log_error( + _function: String, + _file: String, + _line: int, + message: String, + _rationale: String, + _editor_notify: bool, + error_type: int, + script_backtraces: Array[ScriptBacktrace] + ) -> void: + + var stack_trace := GdUnitStackTrace.from_script_backtraces(script_backtraces) + if stack_trace.get_frames().size() == 0: + stack_trace = GdUnitStackTrace.new([ + GdUnitStackTraceElement.new(_file, _line, _function) + ]) + + match error_type: + ErrorType.ERROR_TYPE_WARNING: + if _is_report_push_errors: + _entries.append(ErrorLogEntry.of_push_warning(stack_trace.get_line_number(), message, stack_trace)) + + ErrorType.ERROR_TYPE_ERROR: + if _is_report_push_errors: + _entries.append(ErrorLogEntry.of_push_error(stack_trace.get_line_number(), message, stack_trace)) + + ErrorType.ERROR_TYPE_SCRIPT: + if _is_report_script_errors: + _entries.append(ErrorLogEntry.of_script_error(stack_trace.get_line_number(), message, stack_trace)) + + ErrorType.ERROR_TYPE_SHADER: + pass + _: + prints("Unknwon log type", message) + + func _log_message(_message: String, _error: bool) -> void: + pass + + + + + +func _init() -> void: + super("GdUnitLoggerMonitor") + _logger = GdUnitLogger.new(GdUnitSettings.is_report_push_errors(), GdUnitSettings.is_report_script_errors()) + + +func start() -> void: + clear_logs() + + +func stop() -> void: + pass + + +func log_entries() -> Array[ErrorLogEntry]: + return _logger.entries() + + +func erase_log_entry(log_entry: ErrorLogEntry) -> void: + _logger.erase_log_entry(log_entry) + + +func to_reports() -> Array[GdUnitReport]: + var reports_: Array[GdUnitReport] = [] + + reports_.assign(log_entries().map(_to_report)) + + return reports_ + + +static func _to_report(errorLog: ErrorLogEntry) -> GdUnitReport: + + var failure := """ + %s + %s""".dedent().trim_prefix("\n") % [ + GdAssertMessages._error("Godot Runtime Error !"), + GdAssertMessages._colored_value(errorLog._message)] + var error := GdUnitError.new(failure, errorLog._line, errorLog._stack_trace) + return GdUnitReport.new().from_error(GdUnitReport.ABORT, error) + + +func clear_logs() -> void: + log_entries().clear() diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid new file mode 100644 index 0000000..629f6d8 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid @@ -0,0 +1 @@ +uid://ch4dnlj6y7d6p diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd b/addons/gdUnit4/src/network/GdUnitServer.gd new file mode 100644 index 0000000..4ce2f48 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServer.gd @@ -0,0 +1,41 @@ +@tool +extends Node + +var _client_id: int + +@onready var _server: GdUnitTcpServer = $TcpServer + + +@warning_ignore("return_value_discarded") +func _ready() -> void: + var result := _server.start() + if result.is_error(): + push_error(result.error_message()) + return + var server_port :int = result.value() + Engine.set_meta("gdunit_server_port", server_port) + _server.client_connected.connect(_on_client_connected) + _server.client_disconnected.connect(_on_client_disconnected) + _server.rpc_data.connect(_receive_rpc_data) + + +func _on_client_connected(client_id: int) -> void: + _client_id = client_id + GdUnitSignals.instance().gdunit_client_connected.emit(client_id) + + +func _on_client_disconnected(client_id: int) -> void: + GdUnitSignals.instance().gdunit_client_disconnected.emit(client_id) + + +func _receive_rpc_data(p_rpc: RPC) -> void: + if p_rpc is RPCMessage: + var rpc_message: RPCMessage = p_rpc + GdUnitSignals.instance().gdunit_message.emit(rpc_message.message()) + return + if p_rpc is RPCGdUnitEvent: + var rpc_event: RPCGdUnitEvent = p_rpc + var event := rpc_event.event() + GdUnitSignals.instance().gdunit_event.emit(event) + if event.type() == GdUnitEvent.SESSION_CLOSE and _server != null: + _server.disconnect_client(_client_id) diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd.uid b/addons/gdUnit4/src/network/GdUnitServer.gd.uid new file mode 100644 index 0000000..3b8cf5f --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServer.gd.uid @@ -0,0 +1 @@ +uid://cfme58avgvxqy diff --git a/addons/gdUnit4/src/network/GdUnitServer.tscn b/addons/gdUnit4/src/network/GdUnitServer.tscn new file mode 100644 index 0000000..4dbe8c4 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServer.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=3 format=3 uid="uid://cn5mp3tmi2gb1"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitServer.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpServer.gd" id="2"] + +[node name="Control" type="Node"] +script = ExtResource("1") + +[node name="TcpServer" type="Node" parent="."] +script = ExtResource("2") diff --git a/addons/gdUnit4/src/network/GdUnitServerConstants.gd b/addons/gdUnit4/src/network/GdUnitServerConstants.gd new file mode 100644 index 0000000..d31eee7 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServerConstants.gd @@ -0,0 +1,6 @@ +class_name GdUnitServerConstants +extends RefCounted + +const DEFAULT_SERVER_START_RETRY_TIMES :int = 5 +const GD_TEST_SERVER_PORT :int = 31002 +const JSON_RESPONSE_DELIMITER :String = "<>" diff --git a/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid b/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid new file mode 100644 index 0000000..7bab11f --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid @@ -0,0 +1 @@ +uid://qdhkfp8p22cw diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd b/addons/gdUnit4/src/network/GdUnitTask.gd new file mode 100644 index 0000000..e0188a0 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTask.gd @@ -0,0 +1,25 @@ +class_name GdUnitTask +extends RefCounted + +const TASK_NAME = "task_name" +const TASK_ARGS = "task_args" + +var _task_name :String +var _fref :Callable + + +func _init(task_name :String,instance :Object,func_name :String) -> void: + _task_name = task_name + if not instance.has_method(func_name): + push_error("Can't create GdUnitTask, Invalid func name '%s' for instance '%s'" % [instance, func_name]) + _fref = Callable(instance, func_name) + + +func name() -> String: + return _task_name + + +func execute(args :Array) -> GdUnitResult: + if args.is_empty(): + return _fref.call() + return _fref.callv(args) diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd.uid b/addons/gdUnit4/src/network/GdUnitTask.gd.uid new file mode 100644 index 0000000..0721f9e --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTask.gd.uid @@ -0,0 +1 @@ +uid://cdit06a386ixp diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd b/addons/gdUnit4/src/network/GdUnitTcpClient.gd new file mode 100644 index 0000000..d6c8a42 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd @@ -0,0 +1,126 @@ +@tool +class_name GdUnitTcpClient +extends GdUnitTcpNode + +signal connection_succeeded(message: String) +signal connection_failed(message: String) + + +var _client_name: String +var _debug := false +var _host: String +var _port: int +var _client_id: int +var _connected: bool +var _stream: StreamPeerTCP + + +func _init(client_name := "GdUnit4 TCP Client", debug := false) -> void: + _client_name = client_name + _debug = debug + + +func _ready() -> void: + _connected = false + _stream = StreamPeerTCP.new() + #_stream.set_big_endian(true) + + +func stop() -> void: + console("Disconnecting from server") + if _stream != null: + rpc_send(_stream, RPCClientDisconnect.new().with_id(_client_id)) + if _stream != null: + _stream.disconnect_from_host() + _connected = false + + +func start(host: String, port: int) -> GdUnitResult: + _host = host + _port = port + if _connected: + return GdUnitResult.warn("Client already connected ... %s:%d" % [_host, _port]) + + # Connect client to server + if _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + var err := _stream.connect_to_host(host, port) + #prints("connect_to_host", host, port, err) + if err != OK: + return GdUnitResult.error("GdUnit4: Can't establish client, error code: %s" % err) + return GdUnitResult.success("GdUnit4: Client connected checked port %d" % port) + + +func _process(_delta: float) -> void: + match _stream.get_status(): + StreamPeerTCP.STATUS_NONE: + return + + StreamPeerTCP.STATUS_CONNECTING: + set_process(false) + # wait until client is connected to server + for retry in 10: + @warning_ignore("return_value_discarded") + _stream.poll() + console("Waiting to connect ..") + if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTING: + await get_tree().create_timer(0.500).timeout + if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED: + set_process(true) + return + set_process(true) + _stream.disconnect_from_host() + console("Connection failed") + connection_failed.emit("Connect to TCP Server %s:%d faild!" % [_host, _port]) + + StreamPeerTCP.STATUS_CONNECTED: + if not _connected: + var rpc_data :RPC = null + set_process(false) + while rpc_data == null: + await get_tree().create_timer(0.500).timeout + rpc_data = rpc_receive() + set_process(true) + _client_id = (rpc_data as RPCClientConnect).client_id() + console("Connected to Server: %d" % _client_id) + connection_succeeded.emit("Connect to TCP Server %s:%d success." % [_host, _port]) + _connected = true + process_rpc() + + StreamPeerTCP.STATUS_ERROR: + console("Connection failed") + _stream.disconnect_from_host() + connection_failed.emit("Connect to TCP Server %s:%d faild!" % [_host, _port]) + return + + +func is_client_connected() -> bool: + return _connected + + +func process_rpc() -> void: + if _stream.get_available_bytes() > 0: + var rpc_data := rpc_receive() + if rpc_data is RPCClientDisconnect: + console("RPCClientDisconnect") + GdUnitSignals.instance().gdunit_test_session_terminate.emit.call_deferred() + + +func send(data: RPC) -> void: + rpc_send(_stream, data) + + +func rpc_receive() -> RPC: + return receive_packages(_stream).front() + + +func console(value: Variant) -> void: + if _debug: + print(_client_name, ": ", value) + + +func _on_connection_failed(message: String) -> void: + console("Connection faild by: " + message) + + +func _on_connection_succeeded(message: String) -> void: + console("Connected: " + message) diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid new file mode 100644 index 0000000..8231f45 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid @@ -0,0 +1 @@ +uid://bs742unulu33e diff --git a/addons/gdUnit4/src/network/GdUnitTcpNode.gd b/addons/gdUnit4/src/network/GdUnitTcpNode.gd new file mode 100644 index 0000000..156c764 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpNode.gd @@ -0,0 +1,73 @@ +class_name GdUnitTcpNode +extends Node + + +func rpc_send(stream: StreamPeerTCP, data: RPC) -> void: + var package_buffer := StreamPeerBuffer.new() + var buffer := data.serialize().to_utf16_buffer() + package_buffer.put_u32(0xDEADBEEF) + package_buffer.put_u32(buffer.size()) + var status_code := package_buffer.put_data(buffer) + if status_code != OK: + push_error("'rpc_send:' Can't put_data(), error: %s" % error_string(status_code)) + return + stream.put_data(package_buffer.data_array) + + +func receive_packages(stream: StreamPeerTCP, rpc_cb: Callable = noop) -> Array[RPC]: + var received_packages: Array[RPC] = [] + var package_buffer := StreamPeerBuffer.new() + if stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return received_packages + + while stream.get_status() == StreamPeerTCP.STATUS_CONNECTED and stream.get_available_bytes() > 0: + var buffer := stream.get_data(8) + var status_code: int = buffer[0] + if status_code != OK: + push_error("'receive_packages:' Can't get_data(%d) for available_bytes, error: %s" + % [stream.get_available_bytes(), error_string(status_code)]) + return received_packages + + var data_package: PackedByteArray + package_buffer.data_array = buffer[1] + package_buffer.seek(0) + + if package_buffer.get_u32() == 0xDEADBEEF: + var size := package_buffer.get_u32() + if stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return received_packages + if stream.get_available_bytes() < size: + prints("size check:", + package_buffer.get_size(), ":", + package_buffer.get_position(), + "to read:", + size, + "available size:", + stream.get_available_bytes()) + push_error("'receive_packages:' Can't receive data get_data(%d) for package, error: %s" % [size, error_string(status_code)]) + return received_packages + + buffer = stream.get_data(size) + package_buffer.data_array = buffer[1] + + var rpc_data := package_buffer.get_data(size) + status_code = rpc_data[0] + if status_code != OK: + push_error("'receive_packages:' Can't get_data(%d) for package, error: %s" % [size, error_string(status_code)]) + continue + data_package = rpc_data[1] + else: + data_package = buffer[1] + + var json := data_package.get_string_from_utf16() + if json.is_empty(): + push_warning("json is empty, can't process data") + continue + var data := RPC.deserialize(json) + received_packages.append(data) + rpc_cb.call(data) + return received_packages + + +static func noop(_rpc_data: RPC) -> void: + pass diff --git a/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid new file mode 100644 index 0000000..7ecaa45 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid @@ -0,0 +1 @@ +uid://brw3pa71otmu0 diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd new file mode 100644 index 0000000..997d6e3 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -0,0 +1,140 @@ +@tool +class_name GdUnitTcpServer +extends Node + +signal client_connected(client_id: int) +signal client_disconnected(client_id: int) +@warning_ignore("unused_signal") +signal rpc_data(rpc_data: RPC) + +var _server: TCPServer +var _server_name: String + +class TcpConnection extends GdUnitTcpNode: + var _id: int + var _stream: StreamPeerTCP + + + func _init(tcp_server: TCPServer) -> void: + _stream = tcp_server.take_connection() + #_stream.set_big_endian(true) + _id = _stream.get_instance_id() + rpc_send(_stream, RPCClientConnect.new().with_id(_id)) + + + func _ready() -> void: + server().client_connected.emit(_id) + + + func close() -> void: + if _stream != null and _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED: + _stream.disconnect_from_host() + queue_free() + + + func id() -> int: + return _id + + + func server() -> GdUnitTcpServer: + return get_parent() + + + func _process(_delta: float) -> void: + if _stream == null or _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return + receive_packages(_stream, func(rpc_data: RPC) -> void: + server().rpc_data.emit(rpc_data) + # is client disconnecting we close the server after a timeout of 1 second + if rpc_data is RPCClientDisconnect: + close() + ) + + + func disconnect_from_server() -> void: + if _stream == null or _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return + rpc_send(_stream, RPCClientDisconnect.new().with_id(_id)) + + + func console(_value: Variant) -> void: + #print_debug("TCP Server: ", value) + pass + + +func _init(server_name := "GdUnit4 TCP Server") -> void: + _server_name = server_name + GdUnitSignals.instance().gdunit_test_session_terminate.connect(func() -> void: + for connection in get_children(): + if connection is TcpConnection: + (connection as TcpConnection).disconnect_from_server() + ) + + +func _ready() -> void: + _server = TCPServer.new() + client_connected.connect(_on_client_connected) + client_disconnected.connect(_on_client_disconnected) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + stop() + + +func start(server_port := GdUnitServerConstants.GD_TEST_SERVER_PORT) -> GdUnitResult: + var err := OK + for retry in GdUnitServerConstants.DEFAULT_SERVER_START_RETRY_TIMES: + err = _server.listen(server_port, "127.0.0.1") + if err != OK: + prints("GdUnit4: Can't establish server checked port: %d, Error: %s" % [server_port, error_string(err)]) + server_port += 1 + prints("GdUnit4: Retry (%d) ..." % retry) + else: + break + if err != OK: + if err == ERR_ALREADY_IN_USE: + return GdUnitResult.error("GdUnit4: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) + return GdUnitResult.error("GdUnit4: Can't establish server. Error: %s." % error_string(err)) + console("Successfully started checked port: %d" % server_port) + return GdUnitResult.success(server_port) + + +func stop() -> void: + if _server: + _server.stop() + for connection in get_children(): + if connection is TcpConnection: + @warning_ignore("unsafe_method_access") + connection.close() + remove_child(connection) + _server = null + + +func disconnect_client(client_id: int) -> void: + client_disconnected.emit(client_id) + + +func _process(_delta: float) -> void: + if _server != null and not _server.is_listening(): + return + # check if connection is ready to be used + if _server != null and _server.is_connection_available(): + add_child(TcpConnection.new(_server)) + + +func _on_client_connected(client_id: int) -> void: + console("Client connected %d" % client_id) + + +func _on_client_disconnected(client_id: int) -> void: + for connection in get_children(): + @warning_ignore("unsafe_method_access") + if connection is TcpConnection and connection.id() == client_id: + @warning_ignore("unsafe_method_access") + connection.close() + remove_child(connection) + + +func console(value: Variant) -> void: + print(_server_name, ": ", value) diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid new file mode 100644 index 0000000..0955167 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid @@ -0,0 +1 @@ +uid://76illlyjxg1a diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd b/addons/gdUnit4/src/network/rpc/RPC.gd new file mode 100644 index 0000000..6569cb1 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPC.gd @@ -0,0 +1,37 @@ +class_name RPC +extends RefCounted + + +var _data: Dictionary = {} + + +func _init(obj: Object = null) -> void: + if obj != null: + if obj.has_method("serialize"): + _data = obj.call("serialize") + else: + _data = inst_to_dict(obj) + + +func get_data() -> Object: + return dict_to_inst(_data) + + +func serialize() -> String: + return JSON.stringify(inst_to_dict(self)) + + +# using untyped version see comments below +static func deserialize(json_value: String) -> Object: + var json := JSON.new() + var err := json.parse(json_value) + if err != OK: + push_error("Can't deserialize JSON, error at line %d:\n error: %s \n json: '%s'" + % [json.get_error_line(), json.get_error_message(), json_value]) + return null + var result: Dictionary = json.get_data() + if not typeof(result) == TYPE_DICTIONARY: + push_error("Can't deserialize JSON. Expecting dictionary, error at line %d:\n error: %s \n json: '%s'" + % [result.error_line, result.error_string, json_value]) + return null + return dict_to_inst(result) diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd.uid b/addons/gdUnit4/src/network/rpc/RPC.gd.uid new file mode 100644 index 0000000..79c6aca --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPC.gd.uid @@ -0,0 +1 @@ +uid://c8pff1lad34fp diff --git a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd new file mode 100644 index 0000000..6b494cf --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd @@ -0,0 +1,13 @@ +class_name RPCClientConnect +extends RPC + +var _client_id: int + + +func with_id(id: int) -> RPCClientConnect: + _client_id = id + return self + + +func client_id() -> int: + return _client_id diff --git a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid new file mode 100644 index 0000000..6b110d8 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid @@ -0,0 +1 @@ +uid://bfpgqrpw0srmt diff --git a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd new file mode 100644 index 0000000..7445b9d --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd @@ -0,0 +1,13 @@ +class_name RPCClientDisconnect +extends RPC + +var _client_id: int + + +func with_id(id: int) -> RPCClientDisconnect: + _client_id = id + return self + + +func client_id() -> int: + return _client_id diff --git a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid new file mode 100644 index 0000000..0cfa177 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid @@ -0,0 +1 @@ +uid://jmecylvqpiot diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd new file mode 100644 index 0000000..dbf55c6 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd @@ -0,0 +1,14 @@ +class_name RPCGdUnitEvent +extends RPC + + +static func of(p_event: GdUnitEvent) -> RPCGdUnitEvent: + return RPCGdUnitEvent.new(p_event) + + +func event() -> GdUnitEvent: + return GdUnitEvent.new().deserialize(_data) + + +func _to_string() -> String: + return "RPCGdUnitEvent: " + str(_data) diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid new file mode 100644 index 0000000..4af68da --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid @@ -0,0 +1 @@ +uid://bidsxgasb0u6n diff --git a/addons/gdUnit4/src/network/rpc/RPCMessage.gd b/addons/gdUnit4/src/network/rpc/RPCMessage.gd new file mode 100644 index 0000000..1db0470 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCMessage.gd @@ -0,0 +1,18 @@ +class_name RPCMessage +extends RPC + +var _message: String + + +static func of(msg :String) -> RPCMessage: + var rpc := RPCMessage.new() + rpc._message = msg + return rpc + + +func message() -> String: + return _message + + +func _to_string() -> String: + return "RPCMessage: " + _message diff --git a/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid b/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid new file mode 100644 index 0000000..3605c57 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid @@ -0,0 +1 @@ +uid://dvpuwvi1nfpom diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd new file mode 100644 index 0000000..b55b964 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd @@ -0,0 +1,202 @@ +class_name GdUnitReportSummary +extends RefCounted + +var _resource_path: String +var _name: String +var _test_count := 0 +var _failure_count := 0 +var _error_count := 0 +var _orphan_count := 0 +var _skipped_count := 0 +var _flaky_count := 0 +var _duration := 0 +var _reports: Array[GdUnitReportSummary] = [] +var _text_formatter: Callable + + +func _init(text_formatter: Callable) -> void: + _text_formatter = text_formatter + + +func name() -> String: + return _name + + +func path() -> String: + return _resource_path.get_base_dir().replace("res://", "") + + +func get_resource_path() -> String: + return _resource_path + + +func suite_count() -> int: + return _reports.size() + + +func suite_executed_count() -> int: + var executed := _reports.size() + for report in _reports: + if report.test_count() == report.skipped_count(): + executed -= 1 + return executed + + +func test_count() -> int: + var count := _test_count + for report in _reports: + count += report.test_count() + return count + + +func test_executed_count() -> int: + return test_count() - skipped_count() + + +func success_count() -> int: + return test_count() - error_count() - failure_count() - flaky_count() - skipped_count() + + +func error_count() -> int: + return _error_count + + +func failure_count() -> int: + return _failure_count + + +func skipped_count() -> int: + return _skipped_count + + +func flaky_count() -> int: + return _flaky_count + + +func orphan_count() -> int: + return _orphan_count + + +func duration() -> int: + return _duration + + +func get_reports() -> Array: + return _reports + + +func add_report(report: GdUnitReportSummary) -> void: + _reports.append(report) + + +func report_state() -> String: + return calculate_state(error_count(), failure_count(), orphan_count(), flaky_count(), skipped_count()) + + +func succes_rate() -> String: + return calculate_succes_rate(test_count(), error_count(), failure_count()) + + +@warning_ignore("shadowed_variable") +func add_testcase(resource_path: String, suite_name: String, test_name: String) -> void: + for report: GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == resource_path: + var test_report := GdUnitTestCaseReport.new(resource_path, suite_name, test_name, _text_formatter) + report.add_or_create_test_report(test_report) + + +func add_reports( + p_resource_path: String, + p_test_name: String, + p_reports: Array[GdUnitReport]) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.add_testcase_reports(p_test_name, p_reports) + + +func add_testsuite_report(p_resource_path: String, p_suite_name: String, p_test_count: int) -> void: + _reports.append(GdUnitTestSuiteReport.new(p_resource_path, p_suite_name, p_test_count, _text_formatter)) + + +func add_testsuite_reports( + p_resource_path: String, + p_reports: Array = []) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.set_reports(p_reports) + + +func set_counters( + p_resource_path: String, + p_test_name: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + + for report: GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.set_testcase_counters(p_test_name, p_error_count, p_failure_count, p_orphan_count, + p_is_skipped, p_is_flaky, p_duration) + + +func update_testsuite_counters( + p_resource_path: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report._update_testsuite_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, p_duration) + _update_summary_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, 0) + + +func _update_summary_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_skipped_count + _flaky_count += p_flaky_count + _duration += p_duration + + +func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int, p_flaky_count: int, p_skipped_count: int) -> String: + if p_error_count > 0: + return "ERROR" + if p_failure_count > 0: + return "FAILED" + if p_flaky_count > 0: + return "FLAKY" + if p_orphan_count > 0: + return "WARNING" + if p_skipped_count > 0: + return "SKIPPED" + return "PASSED" + + +func calculate_succes_rate(p_test_count :int, p_error_count :int, p_failure_count :int) -> String: + if p_failure_count == 0: + return "100%" + var count := p_test_count-p_failure_count-p_error_count + if count < 0: + return "0%" + return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" + + +func create_summary(_report_dir :String) -> String: + return "" diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid new file mode 100644 index 0000000..3da6374 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid @@ -0,0 +1 @@ +uid://dcoo56qw363kh diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd new file mode 100644 index 0000000..bf4c96e --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd @@ -0,0 +1,12 @@ +class_name GdUnitReportWriter +extends RefCounted + + +func write(_report_path: String, _report: GdUnitReportSummary) -> String: + assert(false, "'write' is not implemented!") + return "" + + +func output_format() -> String: + assert(false, "'output_format' is not implemented!") + return "" diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid new file mode 100644 index 0000000..0bf26ca --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid @@ -0,0 +1 @@ +uid://2qt1kqx7eb0y diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd new file mode 100644 index 0000000..b64f0e7 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd @@ -0,0 +1,49 @@ +class_name GdUnitTestCaseReport +extends GdUnitReportSummary + + +var _suite_name: String +var _failure_reports: Array[GdUnitReport] = [] + + +func _init(p_resource_path: String, p_suite_name: String, p_test_name: String, text_formatter: Callable) -> void: + _resource_path = p_resource_path + _suite_name = p_suite_name + _name = p_test_name + _text_formatter = text_formatter + + +func suite_name() -> String: + return _suite_name + + +func failure_report() -> String: + var report_message := "" + for report in get_test_reports(): + var stack_trace := "" if report.stack_trace() == null else report.stack_trace().print_stack_trace() + var message := "%s\n%s\n" % [report.message(), stack_trace] + report_message += _text_formatter.call(message) + return report_message + + +func add_testcase_reports(reports: Array[GdUnitReport]) -> void: + _failure_reports.append_array(reports) + + +func set_testcase_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + _error_count = p_error_count + _failure_count = p_failure_count + _orphan_count = p_orphan_count + _skipped_count = p_is_skipped + _flaky_count = p_is_flaky as int + _duration = p_duration + + +func get_test_reports() -> Array[GdUnitReport]: + return _failure_reports diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid new file mode 100644 index 0000000..01fe5e3 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid @@ -0,0 +1 @@ +uid://1r640rygd8on diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd new file mode 100644 index 0000000..46b8193 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd @@ -0,0 +1,112 @@ +class_name GdUnitTestReporter +extends RefCounted + + +var _statistics := {} +var _summary := {} + + +func init_summary() -> void: + _summary["suite_count"] = 0 + _summary["total_count"] = 0 + _summary["error_count"] = 0 + _summary["failed_count"] = 0 + _summary["skipped_count"] = 0 + _summary["flaky_count"] = 0 + _summary["orphan_nodes"] = 0 + _summary["elapsed_time"] = 0 + + +func init_statistics() -> void: + _statistics.clear() + + +func add_test_statistics(event: GdUnitEvent) -> void: + _statistics[event.guid()] = { + "error_count" : event.error_count(), + "failed_count" : event.failed_count(), + "skipped_count" : event.skipped_count(), + "flaky_count" : event.is_flaky() as int, + "orphan_nodes" : event.orphan_nodes() + } + + +func build_test_suite_statisitcs(event: GdUnitEvent) -> Dictionary: + var statistic := { + "total_count" : _statistics.size(), + "error_count" : event.error_count(), + "failed_count" : event.failed_count(), + "skipped_count" : event.skipped_count(), + "flaky_count" : 0, + "orphan_nodes" : event.orphan_nodes() + } + _summary["suite_count"] += 1 + _summary["total_count"] += _statistics.size() + _summary["error_count"] += event.error_count() + _summary["failed_count"] += event.failed_count() + _summary["skipped_count"] += event.skipped_count() + _summary["orphan_nodes"] += event.orphan_nodes() + _summary["elapsed_time"] += event.elapsed_time() + + for key: String in ["error_count", "failed_count", "skipped_count", "flaky_count", "orphan_nodes"]: + var value: int = _statistics.values().reduce(get_value.bind(key), 0 ) + statistic[key] += value + _summary[key] += value + + return statistic + + +func get_value(acc: int, value: Dictionary, key: String) -> int: + return acc + value[key] + + +func processed_suite_count() -> int: + return _summary["suite_count"] + + +func total_test_count() -> int: + return _summary["total_count"] + + +func total_flaky_count() -> int: + return _summary["flaky_count"] + + +func total_error_count() -> int: + return _summary["error_count"] + + +func total_failure_count() -> int: + return _summary["failed_count"] + + +func total_skipped_count() -> int: + return _summary["skipped_count"] + + +func total_orphan_count() -> int: + return _summary["orphan_nodes"] + + +func elapsed_time() -> int: + return _summary["elapsed_time"] + + +func error_count(statistics: Dictionary) -> int: + return statistics["error_count"] + + +func failed_count(statistics: Dictionary) -> int: + return statistics["failed_count"] + + +func orphan_nodes(statistics: Dictionary) -> int: + return statistics["orphan_nodes"] + + +func skipped_count(statistics: Dictionary) -> int: + return statistics["skipped_count"] + + +func flaky_count(statistics: Dictionary) -> int: + return statistics["flaky_count"] diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid new file mode 100644 index 0000000..b0592e4 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid @@ -0,0 +1 @@ +uid://6233qfulan4v diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd new file mode 100644 index 0000000..9be0682 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd @@ -0,0 +1,96 @@ +class_name GdUnitTestSuiteReport +extends GdUnitReportSummary + +var _time_stamp: int +var _failure_reports: Array[GdUnitReport] = [] + + +func _init(p_resource_path: String, p_name: String, p_test_count: int, text_formatter: Callable) -> void: + _resource_path = p_resource_path + _name = p_name + _test_count = p_test_count + _time_stamp = Time.get_unix_time_from_system() as int + _text_formatter = text_formatter + + +func failure_report() -> String: + var report_message := "" + for report in _failure_reports: + report_message += _text_formatter.call(str(report)) + return report_message + + +func set_duration(p_duration :int) -> void: + _duration = p_duration + + +func time_stamp() -> int: + return _time_stamp + + +func duration() -> int: + return _duration + + +func set_skipped(skipped :int) -> void: + _skipped_count += skipped + + +func set_orphans(orphans :int) -> void: + _orphan_count = orphans + + +func set_failed(count :int) -> void: + _failure_count += count + + +func set_reports(failure_reports :Array[GdUnitReport]) -> void: + _failure_reports = failure_reports + + +func add_or_create_test_report(test_report: GdUnitTestCaseReport) -> void: + _reports.append(test_report) + + +func _update_testsuite_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_skipped_count + _flaky_count += p_flaky_count + _duration += p_duration + + +func set_testcase_counters( + test_name: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + if _reports.is_empty(): + return + var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.set_testcase_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration) + + +func add_testcase_reports(test_name: String, reports: Array[GdUnitReport]) -> void: + if reports.is_empty(): + return + # we lookup to latest matching report because of flaky tests could be retry the tests + # and resultis in multipe report entries with the same name + var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.add_testcase_reports(reports) diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid new file mode 100644 index 0000000..ce2ea94 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid @@ -0,0 +1 @@ +uid://dmbgv26lgbjwj diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd new file mode 100644 index 0000000..c42d029 --- /dev/null +++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd @@ -0,0 +1,225 @@ +@tool +class_name GdUnitConsoleTestReporter + + +var test_session: GdUnitTestSession: + get: + return test_session + set(value): + # disconnect first possible connected listener + if test_session != null: + test_session.test_event.disconnect(on_gdunit_event) + # add listening to current session + test_session = value + if test_session != null: + test_session.test_event.connect(on_gdunit_event) + + +var _writer: GdUnitMessageWriter +var _reporter: GdUnitTestReporter = GdUnitTestReporter.new() +var _status_indent := 86 +var _detailed: bool + + +func _init(writer: GdUnitMessageWriter, detailed := false) -> void: + _writer = writer + _writer.clear() + _detailed = detailed + if _detailed: + _status_indent = 20 + + +func clear() -> void: + _writer.clear() + + +func on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + _reporter.init_summary() + + GdUnitEvent.STOP: + _print_summary() + println_message(build_executed_test_suite_msg(processed_suite_count(), processed_suite_count()), Color.DARK_SALMON) + println_message(build_executed_test_case_msg(total_test_count(), total_skipped_count()), Color.DARK_SALMON) + println_message("Total execution time: %s" % LocalTime.elapsed(elapsed_time()), Color.DARK_SALMON) + # We need finally to set the wave effect to enable the animations + _writer.effect(GdUnitMessageWriter.Effect.WAVE).print_at("", 0) + + GdUnitEvent.TESTSUITE_BEFORE: + _reporter.init_statistics() + print_message("Run Test Suite: ", Color.DARK_TURQUOISE) + println_message(event.resource_path(), GdUnitEditorColorTheme.engine_type_color) + + GdUnitEvent.TESTSUITE_AFTER: + if not event.reports().is_empty(): + _writer.indent(1).color(GdUnitEditorColorTheme.engine_type_color).print_message(event._suite_name) + print_message(" > ") + print_message("finalize()", GdUnitEditorColorTheme.function_definition_color) + _print_failure_report(event.reports()) + _print_statistics(_reporter.build_test_suite_statisitcs(event)) + _print_status(event) + println_message("") + if _detailed: + println_message("") + + GdUnitEvent.TESTCASE_BEFORE: + var test := test_session.find_test_by_id(event.guid()) + _print_test_path(test, event.guid()) + if _detailed: + _writer.color(Color.FOREST_GREEN).print_at("STARTED", _status_indent) + println_message("") + + GdUnitEvent.TESTCASE_AFTER: + _reporter.add_test_statistics(event) + if _detailed: + var test := test_session.find_test_by_id(event.guid()) + _print_test_path(test, event.guid()) + _print_status(event) + _print_failure_report(event.reports()) + if _detailed: + println_message("") + + +func _print_test_path(test: GdUnitTestCase, uid: GdUnitGUID) -> void: + if test == null: + prints_warning("Can't print full test info, the test by uid: '%s' was not discovered." % uid) + _writer.indent(1).color(GdUnitEditorColorTheme.engine_type_color).print_message("Test ID: %s" % uid) + return + + var suite_name := test.source_file if _detailed else test.suite_name + _writer.indent(1).color(GdUnitEditorColorTheme.engine_type_color).print_message(suite_name) + print_message(" > ") + print_message(test.display_name, GdUnitEditorColorTheme.function_definition_color) + + +func _print_status(event: GdUnitEvent) -> void: + if event.is_flaky() and event.is_success(): + var retries: int = event.statistic(GdUnitEvent.RETRY_COUNT) + _writer.color(Color.GREEN_YELLOW) \ + .style(GdUnitMessageWriter.ITALIC) \ + .print_at("FLAKY (%d retries)" % retries, _status_indent) + elif event.is_success(): + _writer.color(Color.FOREST_GREEN).print_at("PASSED", _status_indent) + elif event.is_skipped(): + _writer.color(Color.GOLDENROD).style(GdUnitMessageWriter.ITALIC).print_at("SKIPPED", _status_indent) + elif event.is_failed() or event.is_error(): + var retries: int = event.statistic(GdUnitEvent.RETRY_COUNT) + var message := "FAILED (retry %d)" % retries if retries > 1 else "FAILED" + _writer.color(Color.FIREBRICK) \ + .style(GdUnitMessageWriter.BOLD) \ + .effect(GdUnitMessageWriter.Effect.WAVE) \ + .print_at(message, _status_indent) + elif event.is_warning(): + _writer.color(Color.GOLDENROD) \ + .style(GdUnitMessageWriter.UNDERLINE) \ + .print_at("WARNING", _status_indent) + + println_message(" %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE) + + +func _print_failure_report(reports: Array[GdUnitReport]) -> void: + for report in reports: + if ( + report.is_failure() + or report.is_error() + or report.is_warning() + or report.is_skipped() + or report.is_orphan() + ): + _writer.indent(1) \ + .color(Color.DARK_TURQUOISE) \ + .style(GdUnitMessageWriter.BOLD | GdUnitMessageWriter.UNDERLINE) \ + .println_message("Report:") + _writer.indent(2) \ + .color(GdUnitEditorColorTheme.text_color) \ + .println_message(report.message()) \ + .indent(2) \ + .print_stack_trace(report.stack_trace()) + + if not reports.is_empty(): + println_message("") + + +func _print_statistics(statistics: Dictionary) -> void: + print_message("Statistics:", Color.DODGER_BLUE) + print_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" % \ + [statistics["total_count"], + statistics["error_count"], + statistics["failed_count"], + statistics["flaky_count"], + statistics["skipped_count"], + statistics["orphan_nodes"]]) + + +func _print_summary() -> void: + print_message("Overall Summary:", Color.DODGER_BLUE) + _writer \ + .println_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" % [ + total_test_count(), + total_error_count(), + total_failure_count(), + total_flaky_count(), + total_skipped_count(), + total_orphan_count() + ]) + + +func build_executed_test_suite_msg(executed_count: int, total_count: int) -> String: + if executed_count == total_count: + return "Executed test suites: (%d/%d)" % [executed_count, total_count] + return "Executed test suites: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)] + + +func build_executed_test_case_msg(total_count: int, p_skipped_count: int) -> String: + if p_skipped_count == 0: + return "Executed test cases : (%d/%d)" % [total_count, total_count] + return "Executed test cases : (%d/%d), %d skipped" % [total_count - p_skipped_count, total_count, p_skipped_count] + + +func print_message(message: String, color: Color = GdUnitEditorColorTheme.text_color) -> void: + _writer.color(color).print_message(message) + + +func println_message(message: String, color: Color = GdUnitEditorColorTheme.text_color) -> void: + _writer.color(color).println_message(message) + + +func prints_warning(message: String) -> void: + _writer.prints_warning(message) + + +func prints_error(message: String) -> void: + _writer.prints_error(message) + + +func total_test_count() -> int: + return _reporter.total_test_count() + + +func total_error_count() -> int: + return _reporter.total_error_count() + + +func total_failure_count() -> int: + return _reporter.total_failure_count() + + +func total_flaky_count() -> int: + return _reporter.total_flaky_count() + + +func total_skipped_count() -> int: + return _reporter.total_skipped_count() + + +func total_orphan_count() -> int: + return _reporter.total_orphan_count() + + +func processed_suite_count() -> int: + return _reporter.processed_suite_count() + + +func elapsed_time() -> int: + return _reporter.elapsed_time() diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid new file mode 100644 index 0000000..3847b97 --- /dev/null +++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid @@ -0,0 +1 @@ +uid://ddyufmcjwuo5i diff --git a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd new file mode 100644 index 0000000..079b6df --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd @@ -0,0 +1,55 @@ +class_name GdUnitByPathReport +extends GdUnitReportSummary + + +func _init(p_path: String, report_summaries: Array[GdUnitReportSummary]) -> void: + _resource_path = p_path + _reports = report_summaries + + +# -> Dictionary[String, Array[GdUnitReportSummary]] +static func sort_reports_by_path(report_summaries: Array[GdUnitReportSummary]) -> Dictionary: + var by_path := Dictionary() + for report in report_summaries: + var suite_path: String = ProjectSettings.localize_path(report.path()) + var suite_report: Array[GdUnitReportSummary] = by_path.get(suite_path, [] as Array[GdUnitReportSummary]) + suite_report.append(report) + by_path[suite_path] = suite_report + return by_path + + +func path() -> String: + return _resource_path.replace("res://", "").trim_suffix("/") + + +func create_record(report_link: String) -> String: + return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_PATH, self, report_link) + + +func write(report_dir: String) -> String: + calculate_summary() + var output_path := GdUnitHtmlPatterns.create_path_output_path(report_dir, path()) + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/folder_report.html") + var path_report := GdUnitHtmlPatterns.build(template, self, output_path, GdUnitHtmlPatterns.get_path_as_link(self)) + path_report = apply_testsuite_reports(report_dir, path_report, _reports) + GdUnitHtmlPatterns.write_html_file(output_path, path_report) + return output_path + + +func apply_testsuite_reports(report_dir: String, template: String, test_suite_reports: Array[GdUnitReportSummary]) -> String: + var table_records := PackedStringArray() + for report: GdUnitTestSuiteReport in test_suite_reports: + var report_link := GdUnitHtmlPatterns.create_suite_output_path(report_dir, report.path(), report.name()).replace(report_dir, "..") + @warning_ignore("return_value_discarded") + table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) + + +func calculate_summary() -> void: + for report: GdUnitTestSuiteReport in get_reports(): + _error_count += report.error_count() + _failure_count += report.failure_count() + _orphan_count += report.orphan_count() + _skipped_count += report.skipped_count() + _flaky_count += report.flaky_count() + _duration += report.duration() diff --git a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid new file mode 100644 index 0000000..3e99ea7 --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid @@ -0,0 +1 @@ +uid://uv2polw3jfx1 diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlEncoder.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlEncoder.gd new file mode 100644 index 0000000..c6f02ca --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlEncoder.gd @@ -0,0 +1,12 @@ +class_name GdUnitHtmlEncoder +extends RefCounted + + +static func encode(value: String) -> String: + # & must be replaced first to avoid double-encoding the entities we insert + return value\ + .replace("&", "&")\ + .replace("<", "<")\ + .replace(">", ">")\ + .replace('"', """)\ + .replace("'", "'") diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlEncoder.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlEncoder.gd.uid new file mode 100644 index 0000000..c715390 --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlEncoder.gd.uid @@ -0,0 +1 @@ +uid://ds6m4t1oeimh3 diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd new file mode 100644 index 0000000..2401180 --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd @@ -0,0 +1,209 @@ +class_name GdUnitHtmlPatterns +extends RefCounted + +const TABLE_RECORD_TESTSUITE = """ + + ${testsuite_name} + ${report_state_label} + ${test_count} + ${skipped_count} + ${flaky_count} + ${failure_count} + ${orphan_count} + ${duration} + +
+
+
+
+
+
+
+
+ + +""" + +const TABLE_RECORD_PATH = """ + + ${path} + ${report_state_label} + ${test_count} + ${skipped_count} + ${flaky_count} + ${failure_count} + ${orphan_count} + ${duration} + +
+
+
+
+
+
+
+
+ + +""" + + +const TABLE_REPORT_TESTSUITE = """ + + TestSuite hooks + n/a + ${orphan_count} + ${duration} + +
+${failure-report}
+										
+ + +""" + + +const TABLE_RECORD_TESTCASE = """ + + ${testcase_name} + ${report_state_label} + ${skipped_count} + ${orphan_count} + ${duration} + +
+${failure-report}
+										
+ + +""" + + +const TABLE_BY_PATHS = "${report_table_paths}" +const TABLE_BY_TESTSUITES = "${report_table_testsuites}" +const TABLE_BY_TESTCASES = "${report_table_tests}" + +# the report state success, error, warning +const REPORT_STATE = "${report_state}" +const REPORT_STATE_LABEL = "${report_state_label}" +const PATH = "${path}" +const RESOURCE_PATH = "${resource_path}" +const TESTSUITE_COUNT = "${suite_count}" +const TESTCASE_COUNT = "${test_count}" +const FAILURE_COUNT = "${failure_count}" +const FLAKY_COUNT = "${flaky_count}" +const SKIPPED_COUNT = "${skipped_count}" +const ORPHAN_COUNT = "${orphan_count}" +const DURATION = "${duration}" +const FAILURE_REPORT = "${failure-report}" +const SUCCESS_PERCENT = "${success_percent}" + + +const QUICK_STATE_SKIPPED = "${skipped-percent}" +const QUICK_STATE_PASSED = "${passed-percent}" +const QUICK_STATE_FLAKY = "${flaky-percent}" +const QUICK_STATE_ERROR = "${error-percent}" +const QUICK_STATE_FAILED = "${failed-percent}" +const QUICK_STATE_WARNING = "${warning-percent}" + +const TESTSUITE_NAME = "${testsuite_name}" +const TESTCASE_NAME = "${testcase_name}" +const REPORT_LINK = "${report_link}" +const BREADCRUMP_PATH_LINK = "${breadcrumb_path_link}" +const BUILD_DATE = "${buid_date}" + + +static func current_date() -> String: + return Time.get_datetime_string_from_system(true, true) + + +static func build(template: String, report: GdUnitReportSummary, report_link: String, breadcrumb_path_link := "") -> String: + return template\ + .replace(PATH, get_report_path(report))\ + .replace(BREADCRUMP_PATH_LINK, breadcrumb_path_link)\ + .replace(RESOURCE_PATH, report.get_resource_path())\ + .replace(TESTSUITE_NAME, GdUnitHtmlEncoder.encode(report.name()))\ + .replace(TESTSUITE_COUNT, str(report.suite_count()))\ + .replace(TESTCASE_COUNT, str(report.test_count()))\ + .replace(FAILURE_COUNT, str(report.error_count() + report.failure_count()))\ + .replace(FLAKY_COUNT, str(report.flaky_count()))\ + .replace(SKIPPED_COUNT, str(report.skipped_count()))\ + .replace(ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(DURATION, LocalTime.elapsed(report.duration()))\ + .replace(SUCCESS_PERCENT, report.calculate_succes_rate(report.test_count(), report.error_count(), report.failure_count()))\ + .replace(REPORT_STATE, report.report_state().to_lower())\ + .replace(REPORT_STATE_LABEL, report.report_state())\ + .replace(QUICK_STATE_SKIPPED, calculate_percentage(report.test_count(), report.skipped_count()))\ + .replace(QUICK_STATE_PASSED, calculate_percentage(report.test_count(), report.success_count()))\ + .replace(QUICK_STATE_FLAKY, calculate_percentage(report.test_count(), report.flaky_count()))\ + .replace(QUICK_STATE_ERROR, calculate_percentage(report.test_count(), report.error_count()))\ + .replace(QUICK_STATE_FAILED, calculate_percentage(report.test_count(), report.failure_count()))\ + .replace(QUICK_STATE_WARNING, calculate_percentage(report.test_count(), 0))\ + .replace(REPORT_LINK, report_link)\ + .replace(BUILD_DATE, current_date()) + + +static func load_template(template_name: String) -> String: + return FileAccess.open(template_name, FileAccess.READ).get_as_text() + + +static func normalize_path(path: String) -> String: + return path.replace("/", ".").replace(" ", "_") + + +static func create_suite_output_path(report_dir: String, path: String, name: String) -> String: + return "%s/test_suites/%s.%s.html" % [report_dir, normalize_path(path), name] + + +static func create_path_output_path(report_dir: String, path: String) -> String: + return "%s/path/%s.html" % [report_dir, normalize_path(path)] + + +static func write_html_file(output_path: String, content: String) -> void: + var dir := output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(dir) + FileAccess.open(output_path, FileAccess.WRITE).store_string(content) + + +static func get_path_as_link(report: GdUnitReportSummary) -> String: + return "../path/%s.html" % normalize_path(report.path()) + + +static func get_report_path(report: GdUnitReportSummary) -> String: + var path := report.path() + if path.is_empty(): + return "/" + return path + + +static func calculate_percentage(p_test_count: int, count: int) -> String: + if count <= 0: + return "0%" + return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" + + + +static func create_suite_record(report_link: String, report: GdUnitTestSuiteReport) -> String: + return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_TESTSUITE, report, report_link) + + +static func create_test_failure_report(_report_dir: String, report: GdUnitTestCaseReport) -> String: + return GdUnitHtmlPatterns.TABLE_RECORD_TESTCASE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\ + .replace(GdUnitHtmlPatterns.TESTCASE_NAME, report.name())\ + .replace(GdUnitHtmlPatterns.SKIPPED_COUNT, str(report.skipped_count()))\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, GdUnitHtmlEncoder.encode(report.failure_report())) + + +static func create_suite_failure_report(report: GdUnitTestSuiteReport) -> String: + return GdUnitHtmlPatterns.TABLE_REPORT_TESTSUITE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, GdUnitHtmlEncoder.encode(report.failure_report())) diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid new file mode 100644 index 0000000..1a33ae0 --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid @@ -0,0 +1 @@ +uid://viow60qsb8wp diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd new file mode 100644 index 0000000..ea7c5aa --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd @@ -0,0 +1,63 @@ +class_name GdUnitHtmlReportWriter +extends GdUnitReportWriter + + +func output_format() -> String: + return "HTML" + + +func write(report_path: String, report: GdUnitReportSummary) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/index.html") + var to_write := GdUnitHtmlPatterns.build(template, report, "") + to_write = _apply_path_reports(report_path, to_write, report.get_reports()) + to_write = _apply_testsuite_reports(report_path, to_write, report.get_reports()) + # write report + DirAccess.make_dir_recursive_absolute(report_path) + var html_report_file := "%s/index.html" % report_path + FileAccess.open(html_report_file, FileAccess.WRITE).store_string(to_write) + @warning_ignore("return_value_discarded") + GdUnitFileAccess.copy_directory("res://addons/gdUnit4/src/reporters/html/template/css/", report_path + "/css") + return html_report_file + + +func _apply_path_reports(report_dir: String, template: String, report_summaries: Array) -> String: + # Dictionary[String, Array[GdUnitReportSummary]] + var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(report_summaries) + var table_records := PackedStringArray() + var paths: Array[String] = [] + paths.append_array(path_report_mapping.keys()) + paths.sort() + for report_at_path in paths: + var reports: Array[GdUnitReportSummary] = path_report_mapping.get(report_at_path) + var report := GdUnitByPathReport.new(report_at_path, reports) + var report_link: String = report.write(report_dir).replace(report_dir, ".") + @warning_ignore("return_value_discarded") + table_records.append(report.create_record(report_link)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) + + +func _apply_testsuite_reports(report_dir: String, template: String, test_suite_reports: Array[GdUnitReportSummary]) -> String: + var table_records := PackedStringArray() + for report: GdUnitTestSuiteReport in test_suite_reports: + var report_link: String = _write(report_dir, report).replace(report_dir, ".") + @warning_ignore("return_value_discarded") + table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) + + +func _write(report_dir: String, report: GdUnitTestSuiteReport) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/suite_report.html") + template = GdUnitHtmlPatterns.build(template, report, "", GdUnitHtmlPatterns.get_path_as_link(report)) + + var report_output_path := GdUnitHtmlPatterns.create_suite_output_path(report_dir, report.path(), report.name()) + var test_report_table := PackedStringArray() + if not report._failure_reports.is_empty(): + @warning_ignore("return_value_discarded") + test_report_table.append(GdUnitHtmlPatterns.create_suite_failure_report(report)) + for test_report: GdUnitTestCaseReport in report._reports: + @warning_ignore("return_value_discarded") + test_report_table.append(GdUnitHtmlPatterns.create_test_failure_report(report_output_path, test_report)) + + template = template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTCASES, "\n".join(test_report_table)) + GdUnitHtmlPatterns.write_html_file(report_output_path, template) + return report_output_path diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid new file mode 100644 index 0000000..0938bee --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid @@ -0,0 +1 @@ +uid://bj28hl0jcnak0 diff --git a/addons/gdUnit4/src/reporters/html/template/.gdignore b/addons/gdUnit4/src/reporters/html/template/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css b/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css new file mode 100644 index 0000000..17215ff --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css @@ -0,0 +1,66 @@ +.breadcrumb { + display: flex; + border-radius: 6px; + overflow: hidden; + height: 45px; + z-index: 1; + background-color: #9d73eb; + margin-top: 0px; + margin-bottom: 10px; + box-shadow: 0 0 3px black; +} + +.breadcrumb a { + position: relative; + display: flex; + -ms-flex-positive: 1; + flex-grow: 1; + text-decoration: none; + margin: auto; + height: 100%; + color: white; +} + +.breadcrumb a:first-child { + padding-left: 5.2px; +} + +.breadcrumb a:last-child { + padding-right: 5.2px; +} + +.breadcrumb a:after { + content: ""; + position: absolute; + display: inline-block; + width: 45px; + height: 45px; + top: 0; + right: -20px; + background-color: #9d73eb; + border-top-right-radius: 5px; + transform: scale(0.707) rotate(45deg); + box-shadow: 2px -2px rgba(0, 0, 0, 0.25); + z-index: 1; +} + +.breadcrumb a:last-child:after { + content: none; +} + +.breadcrumb a.active, +.breadcrumb a:hover { + background: #b899f2; + color: white; + text-decoration: underline; +} + +.breadcrumb a.active:after, +.breadcrumb a:hover:after { + background: #b899f2; +} + +.breadcrumb span { + margin: inherit; + z-index: 2; +} diff --git a/addons/gdUnit4/src/reporters/html/template/css/logo.png b/addons/gdUnit4/src/reporters/html/template/css/logo.png new file mode 100644 index 0000000..12de79f Binary files /dev/null and b/addons/gdUnit4/src/reporters/html/template/css/logo.png differ diff --git a/addons/gdUnit4/src/reporters/html/template/css/styles.css b/addons/gdUnit4/src/reporters/html/template/css/styles.css new file mode 100644 index 0000000..e92d59b --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/template/css/styles.css @@ -0,0 +1,475 @@ +html, +body { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + font-family: sans-serif; + background-color: white; + height: 100%; +} + +main { + flex-grow: 1; + overflow: auto; + margin: 0 10em; +} + + +header { + color: white; + padding: 1px; + position: relative; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); +} + +.logo { + position: fixed; + top: 20px; + left: 20px; + display: flex; + align-items: center; + z-index: 1000; + filter: grayscale(1); + mix-blend-mode: plus-lighter; +} + +.logo img { + width: 64px; + height: 64px; +} + +.logo span { + font-size: 1.2em; + color: lightslategray; +} + +.report-container { + margin: 0 15em; + text-align: center; + margin-top: 60px; + flex-grow: 0; +} + +h1 { + margin: 0 0 20px 0; + font-size: 2.5em; + font-weight: normal; +} + +.summary { + display: inline-flex; + justify-content: center; + flex-wrap: nowrap; + margin-bottom: 20px; + align-items: baseline; + max-width: 960px; +} + +.summary-item { + flex: 1; + min-width: 80px; +} + +.label { + font-size: 1em; + flex-wrap: nowrap; +} + +.value { + font-size: 0.9em; + display: block; + padding-top: 10px; + color: lightgray; +} + +.success-rate { + padding-left: 40px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.check-icon { + background-color: #34c538; + color: white; + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4em; +} + +.rate-text { + text-align: center; + flex-wrap: nowrap; +} + +.percentage { + font-size: 1.2em; + font-weight: bold; +} + + +nav { + padding: 20px 0px; + font-family: monospace; +} + +nav ul { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + justify-content: flex-start; + border-bottom: 1px solid lightgray; +} + +nav li { + cursor: pointer; + padding: 5px 20px; + font-size: 1.1em; + color: lightslategray; +} + +nav li.active { + color: darkslategray; + border-bottom: 1px solid darkslategray; + font-weight: bold; +} + +div#content { + height: calc(100vh - 400px); +} + + +table { + width: 100%; + height: 100%; + border-collapse: collapse; + overflow: hidden; +} + +thead th { + position: sticky; + top: 0; + background-color: white; + z-index: 1; + border-bottom: 2px solid #ddd; +} + +tbody { + display: block; + /* Limit the height of the table body */ + max-height: calc(100vh - 400px); + /* Enable scrolling on the table body */ + overflow-y: auto; +} + +thead, +tbody tr { + display: table; + width: 100%; + table-layout: fixed; +} + +tbody td { + overflow: hidden; +} + +/* Ensure scrollbar visibility */ +tbody::-webkit-scrollbar { + height: 4px; + width: 14px; +} + +tbody::-webkit-scrollbar-thumb { + background-color: #aaa6a6; + border-radius: 4px; +} + +tbody::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +th, +td { + font-size: .9em; + padding: 5px 0px; + border-bottom: 1px solid #eee; + color: lightslategrey; + text-align: left; + text-wrap: nowrap; + /* Default max and min width for all columns */ + max-width: 150px; + min-width: 80px; + width: 80px; +} + +th { + font-size: 1em; + font-weight: normal; + padding-top: 20px; + color: gray; + text-wrap: nowrap; +} + +.tab-report { + display: grid; + grid-template-columns: 100%; + margin-bottom: 20px; +} + +.tab-report-grid { + display: grid; + grid-template-columns: 70% 30%; + margin-bottom: 20px; +} + + +/* Specific styling for the first column (Testcase) */ +th:first-child, +td:first-child { + padding-left: 5px; + text-align: left; + /* Max width for the first column */ + min-width: 249px; + width: 250px; + /* Enable scrollbar if content exceeds max-width */ + white-space: nowrap; + overflow: auto; +} + +/* Scrollbar styles for first column */ +td:first-child { + overflow-x: auto; + text-overflow: initial; +} + +/* Scrollbar appearance */ +td:first-child::-webkit-scrollbar { + height: 6px; +} + +td:first-child::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 10px; +} + +td:first-child::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +/* Max width for Result column */ +th:nth-child(2), +td:nth-child(2) { + max-width: 140px; + min-width: 140px; + width: 140px; +} + +/* Max width for Quick Results column */ +th:nth-child(9), +td:nth-child(9) { + max-width: 140px; + min-width: 140px; + width: 140px; + padding-right: 10px; +} + +/* Background color for alternating groups */ +.group-bg-1 { + background-color: #f1f1f1; +} + +.group-bg-2 { + background-color: #e0e0e0; +} + +.grid-item { + overflow: auto; + padding-left: 20px; + color: lightslategrey; + max-height: calc(100vh - 350px); +} + +div.tab td.report-column, +th.report-column { + display: none; +} + +/* Result status styles */ +.status { + padding: 2px 40px; + border-radius: 6px; + color: black; + width: 40px; + display: flex; + align-content: center; + align-items: center; +} + +.status-bar { + display: flex; + border-radius: 8px; + overflow: hidden; + height: 20px; + flex-wrap: nowrap; + justify-content: space-evenly; +} + +.status-bar-column { + margin: -2px; + color: black; + display: flex; + align-content: center; + align-items: center; + transition: width 0.3s ease; +} + +.status-skipped { + background-color: #888888; +} + +.status-passed { + background-color: #63bb38; +} + +.status-error { + background-color: #fd1100; +} + +.status-failed { + background-color: #ed594f; +} + +.status-flaky { + background-color: #1d9a1f; +} + +.status-warning { + background-color: #fdda3f; +} + +div.tab tr:hover { + background-color: #d9e7fa; + box-shadow: 0 0 5px black; +} + +div.tab tr.selected { + background-color: #d9e7fa; +} + +div.report-column { + margin-top: 10px; + width: 100%; + text-align: left; +} + +.logging-container { + width: 100%; + height: 100%; +} + +div.godot-report-frame { + margin: 10px; + font-family: monospace; + height: 100%; + background-color: #eee; +} + +div.include-footer { + position: fixed; + bottom: 0; + width: 100%; + display: flex; +} + +footer { + position: static; + left: 0; + bottom: 0; + width: 100%; + white-space: nowrap; + color: lightgray; + font-size: 12px; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); + display: flex; + justify-content: space-between; + align-items: center; +} + +footer p { + padding-left: 10em; +} + +footer .status-legend { + display: flex; + gap: 15px; + width: 500px; +} + +footer a { + color: lightgray; +} + +footer a:hover { + color: whitesmoke; +} + +footer a:visited { + color: whitesmoke; +} + +.status-legend-item { + display: flex; + align-items: center; + gap: 5px; +} + +.status-box { + width: 15px; + height: 15px; + border-radius: 3px; + display: inline-block; +} + +/* Normal link */ +a { + color: lightslategrey; +} + +/* Link when hovered */ +a:hover { + color: #9d73eb; +} + +/* Visited link */ +a:visited { + color: #8058e3; +} + +/* Active link (while being clicked) */ +a:active { + color: #8058e3; + /* Custom color when link is clicked */ +} + + +@media (max-width: 1024px) { + .summary { + flex-direction: column; + } + + nav ul { + flex-wrap: wrap; + } + + nav li { + margin-right: 10px; + margin-bottom: 5px; + } +} diff --git a/addons/gdUnit4/src/reporters/html/template/folder_report.html b/addons/gdUnit4/src/reporters/html/template/folder_report.html new file mode 100644 index 0000000..2cdca67 --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/template/folder_report.html @@ -0,0 +1,122 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + +
+ +
+

Report by Paths

+
+ ${resource_path} +
+
+
+ TestSuites + ${suite_count} +
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
โœ“
+
+ Success Rate + ${success_percent} +
+
+
+
+ +
+ +
+
+
+
+ + + + + + + + + + + + + + + + ${report_table_testsuites} + +
TestSuitesResultTestsSkippedFlakyFailuresOrphansDurationSuccess rate
+
+
+
+
+ +
+

Generated by GdUnit4 at ${buid_date}

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + diff --git a/addons/gdUnit4/src/reporters/html/template/index.html b/addons/gdUnit4/src/reporters/html/template/index.html new file mode 100644 index 0000000..342c8f2 --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/template/index.html @@ -0,0 +1,164 @@ + + + + + + + GdUnit4 Report + + + + +
+ +
+

Summary Report

+
+
+ Test Suites + ${suite_count} +
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
โœ“
+
+ Success Rate + ${success_percent} +
+
+
+
+
+
+ +
+ +
+
+ +
+

Generated by GdUnit4 at ${buid_date}

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/src/reporters/html/template/suite_report.html b/addons/gdUnit4/src/reporters/html/template/suite_report.html new file mode 100644 index 0000000..4746841 --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/template/suite_report.html @@ -0,0 +1,177 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + + + + +
+ +
+

Testsuite Report

+
+ ${resource_path} +
+
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
โœ“
+
+ Success Rate + ${success_percent} +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + ${report_table_tests} + +
TestcaseResultSkippedOrphansDurationReport
+
+
+

Failure Report

+
+
+
+
+ +
+

Generated by GdUnit4 at ${buid_date}

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd new file mode 100644 index 0000000..d2484dc --- /dev/null +++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd @@ -0,0 +1,148 @@ +# This class implements the JUnit XML file format +# based checked https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd +class_name JUnitXmlReportWriter +extends GdUnitReportWriter + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const ATTR_CLASSNAME := "classname" +const ATTR_ERRORS := "errors" +const ATTR_FAILURES := "failures" +const ATTR_HOST := "hostname" +const ATTR_ID := "id" +const ATTR_MESSAGE := "message" +const ATTR_NAME := "name" +const ATTR_PACKAGE := "package" +const ATTR_SKIPPED := "skipped" +const ATTR_FLAKY := "flaky" +const ATTR_TESTS := "tests" +const ATTR_TIME := "time" +const ATTR_TIMESTAMP := "timestamp" +const ATTR_TYPE := "type" + +const HEADER := '\n' + + +func output_format() -> String: + return "XML" + + +func write(report_path: String, report: GdUnitReportSummary) -> String: + var result_file: String = "%s/results.xml" % report_path + DirAccess.make_dir_recursive_absolute(report_path) + var file := FileAccess.open(result_file, FileAccess.WRITE) + if file == null: + push_warning("Can't saving the result to '%s'\n Error: %s" % [result_file, error_string(FileAccess.get_open_error())]) + else: + file.store_string(build_junit_report(report_path, report)) + return result_file + + +func build_junit_report(report_path: String, report: GdUnitReportSummary) -> String: + var iso8601_datetime := Time.get_date_string_from_system() + var test_suites := XmlElement.new("testsuites")\ + .attribute(ATTR_ID, iso8601_datetime)\ + .attribute(ATTR_NAME, report_path.get_file())\ + .attribute(ATTR_TESTS, report.test_count())\ + .attribute(ATTR_FAILURES, report.failure_count())\ + .attribute(ATTR_SKIPPED, report.skipped_count())\ + .attribute(ATTR_FLAKY, report.flaky_count())\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\ + .add_childs(build_test_suites(report)) + var as_string := test_suites.to_xml() + test_suites.dispose() + return HEADER + as_string + + +func build_test_suites(summary: GdUnitReportSummary) -> Array: + var test_suites: Array[XmlElement] = [] + for index in summary.get_reports().size(): + var suite_report :GdUnitTestSuiteReport = summary.get_reports()[index] + var iso8601_datetime := Time.get_datetime_string_from_unix_time(suite_report.time_stamp()) + test_suites.append(XmlElement.new("testsuite")\ + .attribute(ATTR_ID, index)\ + .attribute(ATTR_NAME, suite_report.name())\ + .attribute(ATTR_PACKAGE, suite_report.path())\ + .attribute(ATTR_TIMESTAMP, iso8601_datetime)\ + .attribute(ATTR_HOST, "localhost")\ + .attribute(ATTR_TESTS, suite_report.test_count())\ + .attribute(ATTR_FAILURES, suite_report.failure_count())\ + .attribute(ATTR_ERRORS, suite_report.error_count())\ + .attribute(ATTR_SKIPPED, suite_report.skipped_count())\ + .attribute(ATTR_FLAKY, suite_report.flaky_count())\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(suite_report.duration()))\ + .add_childs(build_test_cases(suite_report))) + return test_suites + + +func build_test_cases(suite_report: GdUnitTestSuiteReport) -> Array: + var test_cases: Array[XmlElement] = [] + for index in suite_report.get_reports().size(): + var report :GdUnitTestCaseReport = suite_report.get_reports()[index] + test_cases.append( XmlElement.new("testcase")\ + .attribute(ATTR_NAME, JUnitXmlReportWriter.encode_xml(report.name()))\ + .attribute(ATTR_CLASSNAME, report.suite_name())\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\ + .add_childs(build_reports(report))) + return test_cases + + +func build_reports(test_report: GdUnitTestCaseReport) -> Array: + var failure_reports: Array[XmlElement] = [] + + for report: GdUnitReport in test_report.get_test_reports(): + if report.is_failure(): + failure_reports.append(XmlElement.new("failure")\ + .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\ + .text(failure_message(report))) + elif report.is_error(): + failure_reports.append(XmlElement.new("error")\ + .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\ + .text(failure_message(report))) + elif report.is_skipped(): + failure_reports.append(XmlElement.new("skipped")\ + .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .text(convert_rtf_to_text(report.message()))) + return failure_reports + + +func failure_message(report: GdUnitReport) -> String: + var stack_trace := "" if report.stack_trace() == null else report.stack_trace().print_stack_trace() + return "%s\n%s" % [convert_rtf_to_text(report.message()), stack_trace] + + +func convert_rtf_to_text(bbcode: String) -> String: + return GdUnitTools.richtext_normalize(bbcode) + + +static func to_type(type: int) -> String: + match type: + GdUnitReport.SUCCESS: + return "SUCCESS" + GdUnitReport.WARN: + return "WARN" + GdUnitReport.FAILURE: + return "FAILURE" + GdUnitReport.ORPHAN: + return "ORPHAN" + GdUnitReport.TERMINATED: + return "TERMINATED" + GdUnitReport.INTERUPTED: + return "INTERUPTED" + GdUnitReport.ABORT: + return "ABORT" + return "UNKNOWN" + + +static func to_time(duration: int) -> String: + return "%4.03f" % (duration / 1000.0) + + +static func encode_xml(value: String) -> String: + return value.xml_escape(true) + + +#static func to_ISO8601_datetime() -> String: + #return "%04d-%02d-%02dT%02d:%02d:%02d" % [date["year"], date["month"], date["day"], date["hour"], date["minute"], date["second"]] diff --git a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid new file mode 100644 index 0000000..5ae536f --- /dev/null +++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid @@ -0,0 +1 @@ +uid://bks71jgqkwsdq diff --git a/addons/gdUnit4/src/reporters/xml/XmlElement.gd b/addons/gdUnit4/src/reporters/xml/XmlElement.gd new file mode 100644 index 0000000..86c7421 --- /dev/null +++ b/addons/gdUnit4/src/reporters/xml/XmlElement.gd @@ -0,0 +1,69 @@ +class_name XmlElement +extends RefCounted + +var _name :String +# Dictionary[String, String] +var _attributes :Dictionary = {} +var _childs :Array[XmlElement] = [] +var _parent :XmlElement = null +var _text :String = "" + + +func _init(name :String) -> void: + _name = name + + +func dispose() -> void: + for child in _childs: + child.dispose() + _childs.clear() + _attributes.clear() + _parent = null + + +func attribute(name :String, value :Variant) -> XmlElement: + _attributes[name] = str(value) + return self + + +func text(p_text :String) -> XmlElement: + _text = p_text if p_text.ends_with("\n") else p_text + "\n" + return self + + +func add_child(child :XmlElement) -> XmlElement: + _childs.append(child) + child._parent = self + return self + + +func add_childs(childs :Array[XmlElement]) -> XmlElement: + for child in childs: + @warning_ignore("return_value_discarded") + add_child(child) + return self + + +func indentation() -> String: + return "" if _parent == null else _parent.indentation() + " " + + +func to_xml() -> String: + var attributes := "" + for key in _attributes.keys() as Array[String]: + attributes += ' {attr}="{value}"'.format({"attr": key, "value": _attributes.get(key)}) + + var childs := "" + for child in _childs: + childs += child.to_xml() + + return "{_indentation}<{name}{attributes}>\n{childs}{text}{_indentation}\n"\ + .format({"name": _name, + "attributes": attributes, + "childs": childs, + "_indentation": indentation(), + "text": cdata(_text)}) + + +func cdata(p_text :String) -> String: + return "" if p_text.is_empty() else "\n".format({"text" : p_text}) diff --git a/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid new file mode 100644 index 0000000..f132aa4 --- /dev/null +++ b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid @@ -0,0 +1 @@ +uid://br720v1u3ks1m diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd new file mode 100644 index 0000000..5d8b631 --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -0,0 +1,154 @@ +class_name GdUnitSpyBuilder +extends GdUnitClassDoubler + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd") +const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"] + + +static func build(to_spy: Variant, debug_write := false) -> Variant: + if GdObjects.is_singleton(to_spy): + @warning_ignore("unsafe_cast") + push_error("Spy on a Singleton is not allowed! '%s'" % (to_spy as Object).get_class()) + return null + + # if resource path load it before + if GdObjects.is_scene_resource_path(to_spy): + var scene_resource_path :String = to_spy + if not FileAccess.file_exists(scene_resource_path): + push_error("Can't build spy on scene '%s'! The given resource not exists!" % scene_resource_path) + return null + var scene_to_spy: PackedScene = load(scene_resource_path) + return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write) + # spy checked PackedScene + if GdObjects.is_scene(to_spy): + var scene_to_spy: PackedScene = to_spy + return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write) + # spy checked a scene instance + if GdObjects.is_instance_scene(to_spy): + @warning_ignore("unsafe_cast") + return spy_on_scene(to_spy as Node, debug_write) + + var excluded_functions := [] + if to_spy is Callable: + @warning_ignore("unsafe_cast") + to_spy = CallableDoubler.new(to_spy as Callable) + excluded_functions = CallableDoubler.excluded_functions() + + var spy := spy_on_script(to_spy, excluded_functions, debug_write) + if spy == null: + return null + var spy_instance: Object = spy.new() + @warning_ignore("unsafe_method_access") + # we do not call the original implementation for _ready and all input function, this is actualy done by the engine + spy_instance.__init(["_input", "_gui_input", "_input_event", "_unhandled_input"]) + @warning_ignore("unsafe_cast") + copy_properties(to_spy as Object, spy_instance) + @warning_ignore("return_value_discarded") + GdUnitObjectInteractions.reset(spy_instance) + return register_auto_free(spy_instance) + + +static func get_class_info(clazz :Variant) -> Dictionary: + var clazz_path := GdObjects.extract_class_path(clazz) + var clazz_name :String = GdObjects.extract_class_name(clazz).value() + return { + "class_name" : clazz_name, + "class_path" : clazz_path + } + + +static func spy_on_script(instance: Variant, function_excludes: PackedStringArray, debug_write: bool) -> GDScript: + if GdArrayTools.is_array_type(instance): + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't build spy checked type '%s'! Spy checked Container Built-In Type not supported!" % type_string(typeof(instance))) + return null + var class_info := get_class_info(instance) + var clazz_name :String = class_info.get("class_name") + var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) + if not GdObjects.is_instance(instance): + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't build spy for class type '%s'! Using an instance instead e.g. 'spy()'" % [clazz_name]) + return null + + @warning_ignore("unsafe_method_access") + var spy_template := SPY_TEMPLATE.source_code.format({ + "instance_id" : abs(instance.get_instance_id()), + "gdunit_source_class": clazz_name if clazz_path.is_empty() else clazz_path[0] + }) + @warning_ignore("unsafe_cast") + var lines := load_template(spy_template, class_info) + @warning_ignore("unsafe_cast") + lines += double_functions(instance as Object, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) + # We disable warning/errors for inferred_declaration + if Engine.get_version_info().hex >= 0x40400: + lines.insert(0, '@warning_ignore_start("inferred_declaration")') + lines.append('@warning_ignore_restore("inferred_declaration")') + + var spy := GDScript.new() + spy.source_code = "\n".join(lines) + spy.resource_name = "Spy%s.gd" % clazz_name + spy.resource_path = GdUnitFileAccess.create_temp_dir("spy") + "/Spy%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] + + if debug_write: + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(spy.resource_path) + @warning_ignore("return_value_discarded") + ResourceSaver.save(spy, spy.resource_path) + var error := spy.reload(true) + if error != OK: + push_error("Unexpected Error!, SpyBuilder error, please contact the developer.") + return null + return spy + + +static func spy_on_scene(scene :Node, debug_write :bool) -> Object: + if scene.get_script() == null: + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't create a spy checked a scene without script '%s'" % scene.get_scene_file_path()) + return null + # buils spy checked original script + @warning_ignore("unsafe_cast") + var scene_script :Object = (scene.get_script() as GDScript).new() + var spy := spy_on_script(scene_script, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write) + scene_script.free() + if spy == null: + return null + + # we need to restore the original script properties to apply after script exchange + var original_properties := {} + for p in scene.get_property_list(): + var property_name: String = p["name"] + var usage: int = p["usage"] + if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE) == PROPERTY_USAGE_SCRIPT_VARIABLE: + original_properties[property_name] = scene.get(property_name) + + # exchage with spy + scene.set_script(spy) + # apply original script properties to the spy + for property_name: String in original_properties.keys(): + scene.set(property_name, original_properties[property_name]) + + @warning_ignore("unsafe_method_access") + scene.__init() + return register_auto_free(scene) + + +static func copy_properties(source :Object, dest :Object) -> void: + for property in source.get_property_list(): + var property_name :String = property["name"] + var property_value :Variant = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid new file mode 100644 index 0000000..fb3e31e --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid @@ -0,0 +1 @@ +uid://gnepktkg0d1e diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd new file mode 100644 index 0000000..e0bcaf2 --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd @@ -0,0 +1,46 @@ +class_name DoubledSpyClassSourceClassName + +const __INSTANCE_ID := "gdunit_doubler_instance_id_{instance_id}" + + +class GdUnitSpyDoublerState: + const __SOURCE_CLASS := "{gdunit_source_class}" + + var excluded_methods := PackedStringArray() + + func _init(excluded_methods__ := PackedStringArray()) -> void: + excluded_methods = excluded_methods__ + + +var __spy_state := GdUnitSpyDoublerState.new() +@warning_ignore("unused_private_class_variable") +var __verifier_instance := GdUnitObjectInteractionsVerifier.new() + + +func __init(__excluded_methods := PackedStringArray()) -> void: + __init_doubler() + __spy_state.excluded_methods = __excluded_methods + + +static func __doubler_state() -> GdUnitSpyDoublerState: + if Engine.has_meta(__INSTANCE_ID): + return Engine.get_meta(__INSTANCE_ID).__spy_state + return null + + +func __init_doubler() -> void: + Engine.set_meta(__INSTANCE_ID, self) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE and Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) + + +static func __get_verifier() -> GdUnitObjectInteractionsVerifier: + return Engine.get_meta(__INSTANCE_ID).__verifier_instance + + +static func __do_call_real_func(__func_name: String) -> bool: + @warning_ignore("unsafe_method_access") + return not __doubler_state().excluded_methods.has(__func_name) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid new file mode 100644 index 0000000..43e85ee --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid @@ -0,0 +1 @@ +uid://dn5qsli2y0f12 diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd new file mode 100644 index 0000000..9e2be9f --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd @@ -0,0 +1,88 @@ +@tool +extends Control + +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const TITLE = "gdUnit4 ${version} Console" + +@onready var header := $VBoxContainer/Header +@onready var title: RichTextLabel = $VBoxContainer/Header/header_title +@onready var output: RichTextLabel = $VBoxContainer/Console/TextEdit + + +var _test_reporter: GdUnitConsoleTestReporter + + +func _ready() -> void: + GdUnitFonts.init_fonts(output) + GdUnit4Version.init_version_label(title) + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_message.connect(_on_gdunit_message) + GdUnitSignals.instance().gdunit_client_connected.connect(_on_gdunit_client_connected) + GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_gdunit_client_disconnected) + _test_reporter = GdUnitConsoleTestReporter.new(GdUnitRichTextMessageWriter.new(output)) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + var instance := GdUnitSignals.instance() + if instance.gdunit_event.is_connected(_on_gdunit_event): + instance.gdunit_event.disconnect(_on_gdunit_event) + if instance.gdunit_message.is_connected(_on_gdunit_event): + instance.gdunit_message.disconnect(_on_gdunit_message) + if instance.gdunit_client_connected.is_connected(_on_gdunit_event): + instance.gdunit_client_connected.disconnect(_on_gdunit_client_connected) + if instance.gdunit_client_disconnected.is_connected(_on_gdunit_event): + instance.gdunit_client_disconnected.disconnect(_on_gdunit_client_disconnected) + + +func setup_update_notification(control: Button) -> void: + if not GdUnitSettings.is_update_notification_enabled(): + _test_reporter.println_message("The search for updates is deactivated.", Color.CORNFLOWER_BLUE) + return + + _test_reporter.print_message("Searching for updates... ", Color.CORNFLOWER_BLUE) + var update_client := GdUnitUpdateClient.new() + add_child(update_client) + var response :GdUnitUpdateClient.HttpResponse = await update_client.request_latest_version() + if response.status() != 200: + _test_reporter.println_message("Information cannot be retrieved from GitHub!", GdUnitEditorColorTheme.state_failure) + _test_reporter.println_message("Error: %s" % response.response(), GdUnitEditorColorTheme.state_failure) + return + var latest_version := update_client.extract_latest_version(response) + if not latest_version.is_greater(GdUnit4Version.current()): + _test_reporter.println_message("GdUnit4 is up-to-date.", GdUnitEditorColorTheme.state_success) + return + + _test_reporter.println_message("A new update is available %s" % latest_version, GdUnitEditorColorTheme.state_warning) + _test_reporter.println_message("Open the GdUnit4 settings and check the update tab.", GdUnitEditorColorTheme.state_warning) + + control.icon = GdUnitUiTools.get_icon("Notification", GdUnitEditorColorTheme.state_warning) + var tween := create_tween() + tween.tween_property(control, "self_modulate", Color.VIOLET, .2).set_trans(Tween.TransitionType.TRANS_LINEAR) + tween.tween_property(control, "self_modulate", GdUnitEditorColorTheme.state_warning, .2).set_trans(Tween.TransitionType.TRANS_BOUNCE) + tween.parallel() + tween.tween_property(control, "scale", Vector2.ONE*1.05, .4).set_trans(Tween.TransitionType.TRANS_LINEAR) + tween.tween_property(control, "scale", Vector2.ONE, .4).set_trans(Tween.TransitionType.TRANS_BOUNCE) + tween.set_loops(-1) + tween.play() + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.SESSION_START: + _test_reporter.test_session = GdUnitTestSession.new(GdUnitTestDiscoverGuard.instance().get_discovered_tests(), "") + GdUnitEvent.SESSION_CLOSE: + _test_reporter.test_session = null + + +func _on_gdunit_client_connected(client_id: int) -> void: + _test_reporter.clear() + _test_reporter.println_message("GdUnit Test Client connected with id: %d" % client_id, GdUnitEditorColorTheme.folder_color) + + +func _on_gdunit_client_disconnected(client_id: int) -> void: + _test_reporter.println_message("GdUnit Test Client disconnected with id: %d" % client_id, GdUnitEditorColorTheme.folder_color) + + +func _on_gdunit_message(message: String) -> void: + _test_reporter.println_message(message, GdUnitEditorColorTheme.folder_color) diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid new file mode 100644 index 0000000..72d4566 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid @@ -0,0 +1 @@ +uid://bm6e7reeyoo0j diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.tscn b/addons/gdUnit4/src/ui/GdUnitConsole.tscn new file mode 100644 index 0000000..060cc97 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitConsole.tscn @@ -0,0 +1,70 @@ +[gd_scene format=4 uid="uid://dm0wvfyeew7vd"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitConsole.gd" id="1"] + +[node name="Control" type="Control" unique_id=1838139573] +use_parent_material = true +clip_contents = true +custom_minimum_size = Vector2(0, 200) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1582573516] +use_parent_material = true +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Header" type="Control" parent="VBoxContainer" unique_id=1823073031] +auto_translate_mode = 2 +custom_minimum_size = Vector2(0, 36) +layout_mode = 2 +size_flags_vertical = 0 +localize_numeral_system = false + +[node name="header_title" type="RichTextLabel" parent="VBoxContainer/Header" unique_id=20617532] +auto_translate_mode = 2 +custom_minimum_size = Vector2(0, 36) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +localize_numeral_system = false +bbcode_enabled = true +scroll_active = false +autowrap_mode = 0 +shortcut_keys_enabled = false +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Console" type="ScrollContainer" parent="VBoxContainer" unique_id=547060555] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="TextEdit" type="RichTextLabel" parent="VBoxContainer/Console" unique_id=1121183605] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +focus_mode = 2 +bbcode_enabled = true +scroll_following = true +context_menu_enabled = true +selection_enabled = true diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd b/addons/gdUnit4/src/ui/GdUnitFonts.gd new file mode 100644 index 0000000..0cb0f5d --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd @@ -0,0 +1,36 @@ +@tool +class_name GdUnitFonts +extends RefCounted + + +static func init_fonts(item: CanvasItem) -> float: + # set default size + item.set("theme_override_font_sizes/font_size", 16) + + if Engine.is_editor_hint(): + var base_control := EditorInterface.get_base_control() + # source modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs + # https://github.com/godotengine/godot/blob/9ee1873ae1e09c217ac24a5800007f63cb895615/editor/editor_log.cpp#L65 + var output_source_mono := base_control.get_theme_font("output_source_mono", "EditorFonts") + var output_source_bold_italic := base_control.get_theme_font("output_source_bold_italic", "EditorFonts") + var output_source_italic := base_control.get_theme_font("output_source_italic", "EditorFonts") + var output_source_bold := base_control.get_theme_font("output_source_bold", "EditorFonts") + var output_source := base_control.get_theme_font("output_source", "EditorFonts") + var settings := EditorInterface.get_editor_settings() + var scale_factor := EditorInterface.get_editor_scale() + var font_size: float = settings.get_setting("interface/editor/main_font_size") + + font_size *= scale_factor + item.set("theme_override_fonts/normal_font", output_source) + item.set("theme_override_fonts/bold_font", output_source_bold) + item.set("theme_override_fonts/italics_font", output_source_italic) + item.set("theme_override_fonts/bold_italics_font", output_source_bold_italic) + item.set("theme_override_fonts/mono_font", output_source_mono) + item.set("theme_override_font_sizes/font_size", font_size) + item.set("theme_override_font_sizes/normal_font_size", font_size) + item.set("theme_override_font_sizes/bold_font_size", font_size) + item.set("theme_override_font_sizes/italics_font_size", font_size) + item.set("theme_override_font_sizes/bold_italics_font_size", font_size) + item.set("theme_override_font_sizes/mono_font_size", font_size) + return font_size + return 16.0 diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid new file mode 100644 index 0000000..0bcdf0b --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid @@ -0,0 +1 @@ +uid://du778dqi42x51 diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd new file mode 100644 index 0000000..f5eae3a --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -0,0 +1,33 @@ +@tool +class_name GdUnitInspecor +extends Control + + +var _command_handler := GdUnitCommandHandler.instance() +var _wait_time := 0.0 + + +func _ready() -> void: + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_event.connect(func(event: GdUnitEvent) -> void: + if event.type() != GdUnitEvent.SESSION_START: + return + + var control: Control = get_parent_control() + # if the tab is floating we dont need to set as current + if control is TabContainer: + var tab_container :TabContainer = control + for tab_index in tab_container.get_tab_count(): + if tab_container.get_tab_title(tab_index) == "GdUnit": + tab_container.set_current_tab(tab_index) + ) + + # Register for editor theme updates + add_child(GdUnitEditorColorTheme.new(), true, Node.INTERNAL_MODE_BACK) + + +func _process(delta: float) -> void: + _wait_time += delta + if _wait_time > 2.0: + _wait_time = 0 + _command_handler._do_process() diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid new file mode 100644 index 0000000..5fc2d61 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid @@ -0,0 +1 @@ +uid://qcg0gbq2ok64 diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.tscn b/addons/gdUnit4/src/ui/GdUnitInspector.tscn new file mode 100644 index 0000000..53c483f --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitInspector.tscn @@ -0,0 +1,68 @@ +[gd_scene format=3 uid="uid://mpo5o6d4uybu"] + +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.tscn" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="1_j37cs"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.tscn" id="2"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.tscn" id="3"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.tscn" id="7"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] + +[node name="GdUnit" type="Control" unique_id=272268338] +clip_contents = true +custom_minimum_size = Vector2(360, 440) +layout_mode = 3 +anchors_preset = 9 +anchor_bottom = 1.0 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +focus_mode = 2 +script = ExtResource("1_j37cs") + +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1550259230] +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 11 +theme_override_constants/separation = 0 + +[node name="Header" type="VBoxContainer" parent="VBoxContainer" unique_id=876343251] +use_parent_material = true +clip_contents = true +layout_mode = 2 + +[node name="ToolBar" parent="VBoxContainer/Header" unique_id=185399834 instance=ExtResource("1")] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ProgressBar" parent="VBoxContainer/Header" unique_id=360281822 instance=ExtResource("2")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 5 + +[node name="StatusBar" parent="VBoxContainer/Header" unique_id=625414847 instance=ExtResource("3")] +layout_mode = 2 +size_flags_horizontal = 1 + +[node name="MainPanel" parent="VBoxContainer" unique_id=1133307707 instance=ExtResource("7")] +unique_name_in_owner = true +clip_contents = true +layout_mode = 2 + +[node name="event_server" parent="." unique_id=756999253 instance=ExtResource("7_721no")] + +[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [7]] +[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [7]] +[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]] +[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]] +[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]] +[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]] +[connection signal="select_orphan_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [9]] +[connection signal="select_orphan_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [9]] +[connection signal="select_skipped_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [2]] +[connection signal="select_skipped_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [2]] +[connection signal="tree_view_mode_changed" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_status_bar_tree_view_mode_changed"] diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd new file mode 100644 index 0000000..00f5bc9 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd @@ -0,0 +1,32 @@ +class_name GdUnitInspectorTreeConstants +extends RefCounted + + +# the inspector panel presantation +enum TREE_VIEW_MODE { + TREE, + FLAT +} + + +# The inspector sort modes +enum SORT_MODE { + UNSORTED, + NAME_ASCENDING, + NAME_DESCENDING, + EXECUTION_TIME +} + + +enum STATE { + INITIAL, + RUNNING, + SKIPPED, + SUCCESS, + WARNING, + FLAKY, + FAILED, + ERROR, + ABORDED, + ORPHAN +} diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid new file mode 100644 index 0000000..1a8c24d --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid @@ -0,0 +1 @@ +uid://bkf6s2rvl6iic diff --git a/addons/gdUnit4/src/ui/GdUnitScriptEditorControls.gd b/addons/gdUnit4/src/ui/GdUnitScriptEditorControls.gd new file mode 100644 index 0000000..0d44051 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitScriptEditorControls.gd @@ -0,0 +1,101 @@ +# A tool to provide extended script editor functionallity +class_name GdUnitScriptEditorControls +extends RefCounted + +# https://github.com/godotengine/godot/blob/master/editor/plugins/script_editor_plugin.h +# the Editor menu popup items +enum { + FILE_NEW, + FILE_NEW_TEXTFILE, + FILE_OPEN, + FILE_REOPEN_CLOSED, + FILE_OPEN_RECENT, + FILE_SAVE, + FILE_SAVE_AS, + FILE_SAVE_ALL, + FILE_THEME, + FILE_RUN, + FILE_CLOSE, + CLOSE_DOCS, + CLOSE_ALL, + CLOSE_OTHER_TABS, + TOGGLE_SCRIPTS_PANEL, + SHOW_IN_FILE_SYSTEM, + FILE_COPY_PATH, + FILE_TOOL_RELOAD_SOFT, + SEARCH_IN_FILES, + REPLACE_IN_FILES, + SEARCH_HELP, + SEARCH_WEBSITE, + HELP_SEARCH_FIND, + HELP_SEARCH_FIND_NEXT, + HELP_SEARCH_FIND_PREVIOUS, + WINDOW_MOVE_UP, + WINDOW_MOVE_DOWN, + WINDOW_NEXT, + WINDOW_PREV, + WINDOW_SORT, + WINDOW_SELECT_BASE = 100 +} + + +# Saves the given script and closes if requested by +# The script is saved when is opened in the editor. +# The script is closed when is set to true. +static func save_an_open_script(script_path: String, close:=false) -> bool: + #prints("save_an_open_script", script_path, close) + if !Engine.is_editor_hint(): + return false + var editor := EditorInterface.get_script_editor() + var editor_popup := _menu_popup() + # search for the script in all opened editor scrips + for open_script in editor.get_open_scripts(): + if open_script.resource_path == script_path: + # select the script in the editor + EditorInterface.edit_script(open_script, 0); + # save and close + editor_popup.id_pressed.emit(FILE_SAVE) + if close: + editor_popup.id_pressed.emit(FILE_CLOSE) + return true + return false + + +# Saves all opened script +static func save_all_open_script() -> void: + if Engine.is_editor_hint(): + _menu_popup().id_pressed.emit(FILE_SAVE_ALL) + + +static func close_open_editor_scripts() -> void: + if Engine.is_editor_hint(): + _menu_popup().id_pressed.emit(CLOSE_ALL) + + +# Edits the given script. +# The script is openend in the current editor and selected in the file system dock. +# The line and column on which to open the script can also be specified. +# The script will be open with the user-configured editor for the script's language which may be an external editor. +static func edit_script(script_path: String, line_number := -1) -> void: + if Engine.is_editor_hint(): + var file_system := EditorInterface.get_resource_filesystem() + file_system.update_file(script_path) + var file_system_dock := EditorInterface.get_file_system_dock() + file_system_dock.navigate_to_path(script_path) + EditorInterface.select_file(script_path) + var script: GDScript = load(script_path) + EditorInterface.edit_script(script, line_number) + + +static func _menu_popup() -> PopupMenu: + @warning_ignore("unsafe_method_access") + return EditorInterface.get_script_editor().get_child(0).get_child(0).get_child(0).get_popup() + + +static func _print_menu(popup: PopupMenu) -> void: + for itemIndex in popup.item_count: + prints("get_item_id", popup.get_item_id(itemIndex)) + prints("get_item_accelerator", popup.get_item_accelerator(itemIndex)) + prints("get_item_shortcut", popup.get_item_shortcut(itemIndex)) + prints("get_item_text", popup.get_item_text(itemIndex)) + prints() diff --git a/addons/gdUnit4/src/ui/GdUnitScriptEditorControls.gd.uid b/addons/gdUnit4/src/ui/GdUnitScriptEditorControls.gd.uid new file mode 100644 index 0000000..db86d9b --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitScriptEditorControls.gd.uid @@ -0,0 +1 @@ +uid://dpr3pfn7e8ww8 diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd b/addons/gdUnit4/src/ui/GdUnitUiTools.gd new file mode 100644 index 0000000..a968d00 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd @@ -0,0 +1,171 @@ +@tool +class_name GdUnitUiTools +extends RefCounted + + +const STATE = GdUnitInspectorTreeConstants.STATE + + +enum ImageFlipMode { + HORIZONTAl, + VERITCAL +} + + +static var _spinner: AnimatedTexture +static var icon_cache: Dictionary[STATE, Texture2D] + + +static func get_state_icon(state: STATE) -> Texture2D: + if icon_cache.has(state): + return icon_cache.get(state) + + if not Engine.is_editor_hint(): + return null + + var icon: Texture2D + match state: + STATE.INITIAL: + icon = get_icon("EditorHandleDisabled", GdUnitEditorColorTheme.state_initial) + STATE.RUNNING: + icon = get_spinner() + STATE.SUCCESS: + icon = get_icon("ImportCheck", GdUnitEditorColorTheme.state_success) + STATE.WARNING: + icon = get_icon("ImportCheck", GdUnitEditorColorTheme.state_warning) + STATE.FAILED: + icon = get_icon("ImportFail", GdUnitEditorColorTheme.state_failure) + STATE.ERROR: + icon = get_icon("StatusError", GdUnitEditorColorTheme.state_error) + STATE.FLAKY: + icon = get_icon("CheckBox", GdUnitEditorColorTheme.state_flaky) + STATE.SKIPPED: + icon = get_icon("EditorHandleDisabled", GdUnitEditorColorTheme.state_skipped) + STATE.ORPHAN: + icon = get_icon("Unlinked", GdUnitEditorColorTheme.state_orphan) + + if icon == null: + push_error("missing icon for state:", STATE.keys()[state]) + + icon_cache[state] = icon + return icon + + +## Returns the icon by name, if it exists. +static func get_icon(icon_name: String, color: = Color.BLACK) -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon := EditorInterface.get_base_control().get_theme_icon(icon_name, "EditorIcons") + if icon == null: + return null + if color != Color.BLACK: + icon = _modulate_texture(icon, color) + return icon + + +## Returns the icon flipped +static func get_flipped_icon(icon_name: String, mode: = ImageFlipMode.HORIZONTAl) -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon := EditorInterface.get_base_control().get_theme_icon(icon_name, "EditorIcons") + if icon == null: + return null + return ImageTexture.create_from_image(_flip_image(icon, mode)) + + +static func get_spinner() -> AnimatedTexture: + if _spinner != null: + return _spinner + _spinner = AnimatedTexture.new() + _spinner.frames = 8 + _spinner.speed_scale = 2.5 + for frame in _spinner.frames: + _spinner.set_frame_texture(frame, get_icon("Progress%d" % (frame+1))) + _spinner.set_frame_duration(frame, 0.2) + return _spinner + + +static func get_color_animated_icon(icon_name: String, from: Color, to: Color) -> AnimatedTexture: + if not Engine.is_editor_hint(): + return null + var texture := AnimatedTexture.new() + texture.frames = 8 + texture.speed_scale = 2.5 + var color := from + for frame in texture.frames: + color = lerp(color, to, 0.2) + texture.set_frame_texture(frame, get_icon(icon_name, color)) + texture.set_frame_duration(frame, .2) + return texture + + +static func get_run_overall_icon() -> Texture2D: + if not Engine.is_editor_hint(): + return null + var icon := EditorInterface.get_base_control().get_theme_icon("Play", "EditorIcons") + var image := _merge_images(icon.get_image(), Vector2i(-2, 0), icon.get_image(), Vector2i(3, 0)) + return ImageTexture.create_from_image(image) + + +static func _modulate_texture(texture: Texture2D, color: Color) -> Texture2D: + var image := _modulate_image(texture.get_image(), color) + return ImageTexture.create_from_image(image) + + +static func _modulate_image(image: Image, color: Color) -> Image: + var data: PackedByteArray = image.data["data"] + for pixel in range(0, data.size(), 4): + var pixel_a := _to_color(data, pixel) + if pixel_a.a8 != 0: + pixel_a = pixel_a.lerp(color, .9) + data[pixel + 0] = pixel_a.r8 + data[pixel + 1] = pixel_a.g8 + data[pixel + 2] = pixel_a.b8 + # data[pixel + 3] = 1 + var output_image := Image.new() + output_image.set_data(image.get_width(), image.get_height(), image.has_mipmaps(), image.get_format(), data) + return output_image + + +static func _merge_images(image1: Image, offset1: Vector2i, image2: Image, offset2: Vector2i) -> Image: + ## we need to fix the image to have the same size to avoid merge conflicts + if image1.get_height() < image2.get_height(): + image1.resize(image2.get_width(), image2.get_height()) + # Create a new Image for the merged result + var merged_image := Image.create(image1.get_width(), image1.get_height(), false, Image.FORMAT_RGBA8) + merged_image.blit_rect(image1, Rect2(Vector2.ZERO, image1.get_size()), offset1) + merged_image.blit_rect_mask(image2, image2, Rect2(Vector2.ZERO, image2.get_size()), offset2) + return merged_image + + +@warning_ignore("narrowing_conversion") +static func _merge_images_scaled(image1: Image, offset1: Vector2i, image2: Image, offset2: Vector2i) -> Image: + ## we need to fix the image to have the same size to avoid merge conflicts + if image1.get_height() < image2.get_height(): + image1.resize(image2.get_width(), image2.get_height()) + # Create a new Image for the merged result + var merged_image := Image.create(image1.get_width(), image1.get_height(), false, image1.get_format()) + merged_image.blend_rect(image1, Rect2(Vector2.ZERO, image1.get_size()), offset1) + @warning_ignore("narrowing_conversion") + #image2.resize(image2.get_width()/1.3, image2.get_height()/1.3) + merged_image.blit_rect_mask(image2, image2, Rect2(Vector2.ZERO, image2.get_size()), offset2) + return merged_image + + +static func _flip_image(texture: Texture2D, mode: ImageFlipMode) -> Image: + var flipped_image := Image.new() + flipped_image.copy_from(texture.get_image()) + if mode == ImageFlipMode.VERITCAL: + flipped_image.flip_x() + else: + flipped_image.flip_y() + return flipped_image + + +static func _to_color(data: PackedByteArray, position: int) -> Color: + var pixel_a := Color() + pixel_a.r8 = data[position + 0] + pixel_a.g8 = data[position + 1] + pixel_a.b8 = data[position + 2] + pixel_a.a8 = data[position + 3] + return pixel_a diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid new file mode 100644 index 0000000..8317db5 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid @@ -0,0 +1 @@ +uid://bsoxp6400qxe8 diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd new file mode 100644 index 0000000..3849e31 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd @@ -0,0 +1,45 @@ +class_name GdUnitContextMenuItem + + +var command_id: String: + set(value): + command_id = value + get: + return command_id + +var name: StringName: + set(value): + name = value + get: + return name + +var visible: Callable: + set(value): + visible = value + get: + return visible + +var icon: Texture2D: + get: + return GdUnitCommandHandler.instance().command_icon(command_id) + + +func _init(p_command_id: String, p_name: StringName, p_is_visible: Callable) -> void: + assert(p_command_id != null and not p_command_id.is_empty(), "(%s) missing command id " % p_command_id) + assert(p_is_visible != null, "(%s) missing parameter 'GdUnitCommand'" % p_name) + + self.command_id = p_command_id + self.name = p_name + self.visible = p_is_visible + + +func shortcut() -> Shortcut: + return GdUnitCommandHandler.instance().command_shortcut(command_id) + + +func is_visible(...args: Array) -> bool: + return visible.callv(args) + + +func execute(...args: Array) -> void: + GdUnitCommandHandler.instance().command_execute(command_id, args) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid new file mode 100644 index 0000000..8d5dc3a --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid @@ -0,0 +1 @@ +uid://ql618nr6cdlq diff --git a/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd new file mode 100644 index 0000000..61d630c --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd @@ -0,0 +1,45 @@ +@tool +extends EditorContextMenuPlugin + +var _context_menus: Array[GdUnitContextMenuItem] = [] + + +func _init() -> void: + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + if script == null: + return false + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + # setup shortcuts + _context_menus.append(GdUnitContextMenuItem.new( + GdUnitCommandFileSystemRunTests.ID, + "Run Testsuites", + is_test_suite.bind(true) + )) + _context_menus.append(GdUnitContextMenuItem.new( + GdUnitCommandFileSystemDebugTests.ID, + "Debug Testsuites", + is_test_suite.bind(true) + )) + + +func _popup_menu(paths: PackedStringArray) -> void: + # Filter for directories and test suite files + var valid_paths := Array(paths)\ + .filter(func(path: String) -> bool: + if DirAccess.dir_exists_absolute(path): + return true + if path.get_extension() in ["gd", "cs"]: + var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(path) + return GdUnitTestSuiteScanner.is_test_suite(script) + return false) + + # If no valid paths selected don't extend the context menu + if valid_paths.is_empty(): + return + + for menu_item in _context_menus: + if menu_item.shortcut(): + add_menu_shortcut(menu_item.shortcut(), menu_item.execute.bindv(valid_paths).unbind(1)) + add_context_menu_item_from_shortcut(menu_item.name, menu_item.shortcut(), menu_item.icon) + else: + add_context_menu_item(menu_item.name, menu_item.execute.bindv(valid_paths).unbind(1), menu_item.icon) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd.uid new file mode 100644 index 0000000..f6a95a7 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd.uid @@ -0,0 +1 @@ +uid://cahmormcooriw diff --git a/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd new file mode 100644 index 0000000..2d6071a --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd @@ -0,0 +1,63 @@ +@tool +class_name GdUnitInspectorContextMenu +extends PopupMenu + + +const CONTEXT_MENU_RUN_ID = 0 +const CONTEXT_MENU_DEBUG_ID = 1 +const CONTEXT_MENU_RERUN_UNTIL_ID = 2 +# id 3 is the seperator +const CONTEXT_MENU_COLLAPSE_ALL = 4 +const CONTEXT_MENU_EXPAND_ALL = 5 + + +var command_handler: GdUnitCommandHandler + + +func _ready() -> void: + if not Engine.is_editor_hint(): + return + command_handler = GdUnitCommandHandler.instance() + _setup_item(CONTEXT_MENU_RUN_ID, "Run Tests", GdUnitCommandInspectorRunTests.ID) + _setup_item(CONTEXT_MENU_DEBUG_ID, "Debug Tests", GdUnitCommandInspectorDebugTests.ID) + _setup_item(CONTEXT_MENU_RERUN_UNTIL_ID, "Run Tests Until Fail", GdUnitCommandInspectorRerunTestsUntilFailure.ID) + _setup_item(CONTEXT_MENU_EXPAND_ALL, "Expand All", GdUnitCommandInspectorTreeExpand.ID) + _setup_item(CONTEXT_MENU_COLLAPSE_ALL, "Collapse All", GdUnitCommandInspectorTreeCollapse.ID) + + +func _setup_item(item_id: int, item_name: String, command_id: String) -> void: + set_item_text(item_id, item_name) + set_item_icon(item_id, command_handler.command_icon(command_id)) + set_item_shortcut(item_id, command_handler.command_shortcut(command_id)) + + +func disable_items() -> void: + set_item_disabled(CONTEXT_MENU_RUN_ID, true) + set_item_disabled(CONTEXT_MENU_DEBUG_ID, true) + set_item_disabled(CONTEXT_MENU_RERUN_UNTIL_ID, true) + + +func enable_items() -> void: + set_item_disabled(CONTEXT_MENU_RUN_ID, false) + set_item_disabled(CONTEXT_MENU_DEBUG_ID, false) + set_item_disabled(CONTEXT_MENU_RERUN_UNTIL_ID, false) + + +func _on_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_index: int, source: Tree) -> void: + if mouse_button_index == MOUSE_BUTTON_RIGHT: + position = source.get_screen_position() + mouse_position + popup() + + +func _on_index_pressed(index: int) -> void: + match index: + CONTEXT_MENU_RUN_ID: + command_handler.command_execute(GdUnitCommandInspectorRunTests.ID) + CONTEXT_MENU_DEBUG_ID: + command_handler.command_execute(GdUnitCommandInspectorDebugTests.ID) + CONTEXT_MENU_RERUN_UNTIL_ID: + command_handler.command_execute(GdUnitCommandInspectorRerunTestsUntilFailure.ID) + CONTEXT_MENU_EXPAND_ALL: + command_handler.command_execute(GdUnitCommandInspectorTreeExpand.ID) + CONTEXT_MENU_COLLAPSE_ALL: + command_handler.command_execute(GdUnitCommandInspectorTreeCollapse.ID) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd.uid new file mode 100644 index 0000000..63e853d --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd.uid @@ -0,0 +1 @@ +uid://y116n7hnsd6p diff --git a/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn new file mode 100644 index 0000000..5d87a41 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn @@ -0,0 +1,70 @@ +[gd_scene format=3 uid="uid://bjcc71p2h1trr"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd" id="1_0x0vc"] + +[sub_resource type="DPITexture" id="DPITexture_miuuy"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[sub_resource type="DPITexture" id="DPITexture_ern2r"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[sub_resource type="DPITexture" id="DPITexture_qdci2"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[sub_resource type="DPITexture" id="DPITexture_hed0i"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[node name="contextMenu" type="PopupMenu" unique_id=1824556050] +auto_translate_mode = 2 +oversampling_override = 1.0 +size = Vector2i(241, 170) +visible = true +item_count = 6 +item_0/text = "Run Tests" +item_0/icon = SubResource("DPITexture_miuuy") +item_0/id = 0 +item_1/text = "Debug Tests" +item_1/icon = SubResource("DPITexture_ern2r") +item_1/id = 1 +item_2/text = "Run Tests Until Fail" +item_2/icon = SubResource("DPITexture_miuuy") +item_2/id = 2 +item_3/id = 3 +item_3/separator = true +item_4/text = "Collapse All" +item_4/icon = SubResource("DPITexture_qdci2") +item_4/id = 4 +item_5/text = "Expand All" +item_5/icon = SubResource("DPITexture_hed0i") +item_5/id = 5 +script = ExtResource("1_0x0vc") + +[connection signal="index_pressed" from="." to="." method="_on_index_pressed"] diff --git a/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd new file mode 100644 index 0000000..dc16e12 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd @@ -0,0 +1,38 @@ +@tool +extends EditorContextMenuPlugin + + +var _context_menus: Array[GdUnitContextMenuItem] = [] + + +func _init() -> void: + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + # setup shortcuts + _context_menus.append(GdUnitContextMenuItem.new( + GdUnitCommandScriptEditorRunTests.ID, + "Run Tests", + is_test_suite.bind(true) + )) + _context_menus.append(GdUnitContextMenuItem.new( + GdUnitCommandScriptEditorDebugTests.ID, + "Debug Tests", + is_test_suite.bind(true) + )) + _context_menus.append(GdUnitContextMenuItem.new( + GdUnitCommandScriptEditorCreateTest.ID, + "Create Test", + is_test_suite.bind(false) + )) + + +func _popup_menu(_paths: PackedStringArray) -> void: + var current_script := EditorInterface.get_script_editor().get_current_script() + + for menu_item in _context_menus: + if menu_item.is_visible(current_script): + if menu_item.shortcut(): + add_menu_shortcut(menu_item.shortcut(), menu_item.execute.unbind(1)) + add_context_menu_item_from_shortcut(menu_item.name, menu_item.shortcut()) + else: + add_context_menu_item(menu_item.name, menu_item.execute.unbind(1)) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd.uid new file mode 100644 index 0000000..947e27e --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd.uid @@ -0,0 +1 @@ +uid://dpxvtbedtldnr diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd b/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd new file mode 100644 index 0000000..d1dcc09 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd @@ -0,0 +1,56 @@ +@tool +extends ProgressBar + + +@onready var progress_counter: Label = %progess_counter +@onready var style: StyleBoxFlat = get("theme_override_styles/fill") + +var _state: GdUnitInspectorTreeConstants.STATE + + +func _ready() -> void: + style.bg_color = Color.TRANSPARENT + value = 0 + max_value = 0 + update_text() + # register for progress changes + if Engine.is_editor_hint(): + @warning_ignore("unsafe_property_access", "unsafe_method_access") + if get_parent().get_parent().get_parent().find_child("MainPanel", true, false).test_counters_changed.connect(_on_test_counter_changed) != OK: + push_error("ProgressBar: Can't connect to MainPanel") + + +func update_text() -> void: + progress_counter.text = "%d:%d" % [value, max_value] + + +func _on_test_counter_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE) -> void: + value = index + max_value = total + update_text() + + # inital state + if index == 0: + _state = GdUnitInspectorTreeConstants.STATE.INITIAL + style.bg_color = Color.TRANSPARENT + + # do only update the state is higher prio than current state + if state <= _state: + return + _state = state + + if is_failed(state): + style.bg_color = Color.DARK_RED + else: + style.bg_color = Color.DARK_GREEN + + +func is_failed(state: GdUnitInspectorTreeConstants.STATE) -> bool: + return state in [ + GdUnitInspectorTreeConstants.STATE.FAILED, + GdUnitInspectorTreeConstants.STATE.ERROR, + GdUnitInspectorTreeConstants.STATE.ABORDED] + + +func is_flaky(state: GdUnitInspectorTreeConstants.STATE) -> bool: + return state == GdUnitInspectorTreeConstants.STATE.FLAKY diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd.uid b/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd.uid new file mode 100644 index 0000000..5a2fde7 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd.uid @@ -0,0 +1 @@ +uid://5w72r4buu2ds diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.tscn b/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.tscn new file mode 100644 index 0000000..851a5b6 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.tscn @@ -0,0 +1,49 @@ +[gd_scene format=3 uid="uid://dva3tonxsxrlk"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd" id="1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ayfir"] +bg_color = Color(1, 1, 1, 0) + +[node name="MarginContainer" type="MarginContainer" unique_id=542070056] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 2 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 2 + +[node name="ProgressBar" type="ProgressBar" parent="." unique_id=419200362] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 9 +theme_override_styles/fill = SubResource("StyleBoxFlat_ayfir") +allow_greater = true +show_percentage = false +script = ExtResource("1") + +[node name="progess_counter" type="Label" parent="ProgressBar" unique_id=648033469] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -11.5 +offset_top = -11.5 +offset_right = 11.5 +offset_bottom = 11.5 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 6 +size_flags_vertical = 6 +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_trim_flags = 0 +justification_flags = 0 +max_lines_visible = 1 diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.gd new file mode 100644 index 0000000..0a89b39 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.gd @@ -0,0 +1,301 @@ +@tool +extends Control + +signal select_failure_next() +signal select_failure_prevous() +signal select_error_next() +signal select_error_prevous() +signal select_flaky_next() +signal select_flaky_prevous() +signal select_skipped_next() +signal select_skipped_prevous() +signal select_orphan_next() +signal select_orphan_prevous() + +@warning_ignore("unused_signal") +signal tree_view_mode_changed(flat :bool) + + +@onready var _button_sync: Button = %btn_tree_sync +@onready var _button_view_mode: MenuButton = %btn_tree_mode +@onready var _button_sort_mode: MenuButton = %btn_tree_sort +@onready var _icon_template: SpinBox = %icon_template +@onready var time_value: Label = %time_value +@onready var time_icon: TextureRect = %time_icon + +const STATE = GdUnitInspectorTreeConstants.STATE + +var total_failed := 0 +var total_errors := 0 +var total_flaky := 0 +var total_skipped := 0 +var total_orphans := 0 +var _timer: LocalTime + + +var icon_mappings := { + # tree sort modes + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED : GdUnitUiTools.get_icon("TripleBar"), + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.NAME_ASCENDING : GdUnitUiTools.get_icon("Sort"), + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.NAME_DESCENDING : GdUnitUiTools.get_flipped_icon("Sort"), + 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME : GdUnitUiTools.get_icon("History"), + # tree view modes + 0x200 + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE : GdUnitUiTools.get_icon("Tree", Color.GHOST_WHITE), + 0x200 + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT : GdUnitUiTools.get_icon("AnimationTrackGroup", Color.GHOST_WHITE) +} + + +@warning_ignore("return_value_discarded") +func _ready() -> void: + _button_sync.icon = GdUnitUiTools.get_icon("Loop") + _set_sort_mode_menu_options() + _set_view_mode_menu_options() + _init_status_pannels() + time_value.text = "0ms" + time_icon.texture = GdUnitUiTools.get_icon("Time") + + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) + + +func _process(_delta: float) -> void: + if _timer: + time_value.text = _timer.elapsed_since() + + +func _init_status_pannels() -> void: + _init_statistic_panels(STATE.FAILED) + _init_statistic_panels(STATE.ERROR) + _init_statistic_panels(STATE.FLAKY) + _init_statistic_panels(STATE.SKIPPED) + _init_statistic_panels(STATE.ORPHAN) + + +func _notification(what: int) -> void: + if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: + _init_status_pannels() + + +func _init_statistic_panels(state: STATE) -> void: + var panel: Control = find_child(str(STATE.keys()[state]), true, false) + var icon: TextureRect = panel.find_child("icon") + if icon.texture == null: + icon.texture = GdUnitUiTools.get_state_icon(state) + icon.tooltip_text = panel.get_tooltip_text() + var value: LineEdit = panel.find_child("value") + value.text = "0" + value.tooltip_text = panel.get_tooltip_text() + var btn_up: TextureButton = panel.find_child("up") + var btn_down: TextureButton = panel.find_child("down") + var up_icon := _icon_template.get_theme_icon("up_hover") + var texture_normal := ImageTexture.create_from_image( + GdUnitUiTools._modulate_image(up_icon.get_image(), + Color.GRAY) + ) + var texture_hover := ImageTexture.create_from_image( + GdUnitUiTools._modulate_image(up_icon.get_image(), + Color.WHITE) + ) + var texture_pressed := ImageTexture.create_from_image( + GdUnitUiTools._modulate_image(up_icon.get_image(), + Color.SKY_BLUE) + ) + + btn_up.texture_normal = texture_normal + btn_up.texture_hover = texture_hover + btn_up.texture_pressed = texture_pressed + btn_down.texture_normal = texture_normal + btn_down.texture_hover = texture_hover + btn_down.texture_pressed = texture_pressed + btn_up.pressed.connect(_on_button_up.bind(state)) + btn_down.pressed.connect(_on_button_down.bind(state)) + + +func _on_button_up(state: STATE) -> void: + match state: + STATE.FAILED: + select_failure_prevous.emit() + STATE.ERROR: + select_error_prevous.emit() + STATE.FLAKY: + select_flaky_prevous.emit() + STATE.SKIPPED: + select_skipped_prevous.emit() + STATE.ORPHAN: + select_orphan_prevous.emit() + + +func _on_button_down(state: STATE) -> void: + match state: + STATE.FAILED: + select_failure_next.emit() + STATE.ERROR: + select_error_next.emit() + STATE.FLAKY: + select_flaky_next.emit() + STATE.SKIPPED: + select_skipped_next.emit() + STATE.ORPHAN: + select_orphan_next.emit() + + +func _update_statistics(state: STATE, count: int) -> void: + var panel: Node = find_child(str(STATE.keys()[state]), true, false) + var value: LineEdit = panel.find_child("value") + value.text = str(count) + + +func _set_sort_mode_menu_options() -> void: + _button_sort_mode.icon = GdUnitUiTools.get_icon("Sort") + # construct context sort menu according to the available modes + var context_menu :PopupMenu = _button_sort_mode.get_popup() + context_menu.clear() + + if not context_menu.index_pressed.is_connected(_on_sort_mode_changed): + @warning_ignore("return_value_discarded") + context_menu.index_pressed.connect(_on_sort_mode_changed) + + var configured_sort_mode := GdUnitSettings.get_inspector_tree_sort_mode() + for sort_mode: String in GdUnitInspectorTreeConstants.SORT_MODE.keys(): + var enum_value :int = GdUnitInspectorTreeConstants.SORT_MODE.get(sort_mode) + var icon :Texture2D = icon_mappings[0x100 + enum_value] + context_menu.add_icon_check_item(icon, normalise(sort_mode), enum_value) + context_menu.set_item_checked(enum_value, configured_sort_mode == enum_value) + + +func _set_view_mode_menu_options() -> void: + _button_view_mode.icon = GdUnitUiTools.get_icon("Tree", Color.GHOST_WHITE) + # construct context tree view menu according to the available modes + var context_menu :PopupMenu = _button_view_mode.get_popup() + context_menu.clear() + + if not context_menu.index_pressed.is_connected(_on_tree_view_mode_changed): + @warning_ignore("return_value_discarded") + context_menu.index_pressed.connect(_on_tree_view_mode_changed) + + var configured_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode() + for tree_view_mode: String in GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys(): + var enum_value :int = GdUnitInspectorTreeConstants.TREE_VIEW_MODE.get(tree_view_mode) + var icon :Texture2D = icon_mappings[0x200 + enum_value] + context_menu.add_icon_check_item(icon, normalise(tree_view_mode), enum_value) + context_menu.set_item_checked(enum_value, configured_tree_view_mode == enum_value) + + +func normalise(value: String) -> String: + var parts := value.to_lower().split("_") + parts[0] = parts[0].capitalize() + return " ".join(parts) + + +func status_changed(errors: int, failed: int, flaky: int, skipped: int, orphans: int) -> void: + total_failed += failed + total_errors += errors + total_flaky += flaky + total_skipped += skipped + total_orphans += orphans + _update_statistics(STATE.FAILED, total_failed) + _update_statistics(STATE.ERROR, total_errors) + _update_statistics(STATE.FLAKY, total_flaky) + _update_statistics(STATE.SKIPPED, total_skipped) + _update_statistics(STATE.ORPHAN, total_orphans) + + +func disable_buttons(value :bool) -> void: + _button_sync.set_disabled(value) + _button_sort_mode.set_disabled(value) + _button_view_mode.set_disabled(value) + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.DISCOVER_START: + disable_buttons(true) + + GdUnitEvent.DISCOVER_END: + disable_buttons(false) + + GdUnitEvent.INIT: + total_errors = 0 + total_failed = 0 + total_flaky = 0 + total_skipped = 0 + total_orphans = 0 + status_changed(total_errors, total_failed, total_flaky, total_skipped, total_orphans) + + GdUnitEvent.TESTCASE_AFTER: + status_changed(event.error_count(), event.failed_count(), event.is_flaky(), event.is_skipped(), event.orphan_nodes()) + + GdUnitEvent.TESTSUITE_AFTER: + status_changed(event.error_count(), event.failed_count(), event.is_flaky(), 0, event.orphan_nodes()) + + GdUnitEvent.SESSION_START: + disable_buttons(true) + _timer = LocalTime.now() + + GdUnitEvent.SESSION_CLOSE, GdUnitEvent.STOP: + disable_buttons(false) + _timer = null + + +func _on_btn_error_up_pressed() -> void: + select_error_prevous.emit() + + +func _on_btn_error_down_pressed() -> void: + select_error_next.emit() + + +func _on_failure_up_pressed() -> void: + select_failure_prevous.emit() + + +func _on_failure_down_pressed() -> void: + select_failure_next.emit() + + +func _on_btn_flaky_up_pressed() -> void: + select_flaky_prevous.emit() + + +func _on_btn_flaky_down_pressed() -> void: + select_flaky_next.emit() + + +func _on_btn_skipped_up_pressed() -> void: + select_skipped_prevous.emit() + + +func _on_btn_skipped_down_pressed() -> void: + select_skipped_next.emit() + + +func _on_btn_orphan_up_pressed() -> void: + select_orphan_prevous.emit() + + +func _on_btn_orphan_down_pressed() -> void: + select_orphan_next.emit() + + +func _on_btn_tree_sync_pressed() -> void: + await GdUnitTestDiscoverer.run() + + +func _on_sort_mode_changed(index: int) -> void: + var selected_sort_mode :GdUnitInspectorTreeConstants.SORT_MODE = GdUnitInspectorTreeConstants.SORT_MODE.values()[index] + GdUnitSettings.set_inspector_tree_sort_mode(selected_sort_mode) + + +func _on_tree_view_mode_changed(index: int) ->void: + var selected_tree_mode :GdUnitInspectorTreeConstants.TREE_VIEW_MODE = GdUnitInspectorTreeConstants.TREE_VIEW_MODE.values()[index] + GdUnitSettings.set_inspector_tree_view_mode(selected_tree_mode) + + +################################################################################ +# external signal receiver +################################################################################ +func _on_settings_changed(property :GdUnitProperty) -> void: + if property.name() == GdUnitSettings.INSPECTOR_TREE_SORT_MODE: + _set_sort_mode_menu_options() + if property.name() == GdUnitSettings.INSPECTOR_TREE_VIEW_MODE: + _set_view_mode_menu_options() diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.gd.uid b/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.gd.uid new file mode 100644 index 0000000..385dcc4 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.gd.uid @@ -0,0 +1 @@ +uid://jvyhi0vacq1u diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.tscn b/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.tscn new file mode 100644 index 0000000..7d6f89a --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.tscn @@ -0,0 +1,362 @@ +[gd_scene format=3 uid="uid://c22l4odk7qesc"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.gd" id="3"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_mb3ih"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wo03e"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jh28t"] + +[node name="StatusBar" type="VBoxContainer" unique_id=214621769] +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 132.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +size_flags_vertical = 2 +script = ExtResource("3") + +[node name="tree_tools" type="HBoxContainer" parent="." unique_id=1524945370] +layout_mode = 2 +size_flags_vertical = 0 + +[node name="Label" type="Label" parent="tree_tools" unique_id=226892195] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Statistics:" +autowrap_trim_flags = 0 +justification_flags = 0 + +[node name="timings" type="HBoxContainer" parent="tree_tools" unique_id=1494080859] +layout_mode = 2 +size_flags_horizontal = 2 +size_flags_vertical = 4 +alignment = 1 + +[node name="time_icon" type="TextureRect" parent="tree_tools/timings" unique_id=156277722] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +stretch_mode = 2 + +[node name="time_value" type="Label" parent="tree_tools/timings" unique_id=387690970] +unique_name_in_owner = true +custom_minimum_size = Vector2(64, 0) +layout_mode = 2 +size_flags_horizontal = 3 +text = "0ms" +horizontal_alignment = 2 +vertical_alignment = 1 +autowrap_trim_flags = 0 +justification_flags = 0 + +[node name="tree_buttons" type="HBoxContainer" parent="tree_tools" unique_id=1600307316] +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +alignment = 2 + +[node name="VSeparator" type="VSeparator" parent="tree_tools/tree_buttons" unique_id=1191926359] +layout_mode = 2 + +[node name="btn_tree_sync" type="Button" parent="tree_tools/tree_buttons" unique_id=2138788372] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Run discover tests." +theme_override_styles/normal = SubResource("StyleBoxEmpty_mb3ih") + +[node name="btn_tree_sort" type="MenuButton" parent="tree_tools/tree_buttons" unique_id=1452127838] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Sets tree sorting mode." +theme_override_styles/normal = SubResource("StyleBoxEmpty_wo03e") +flat = false + +[node name="btn_tree_mode" type="MenuButton" parent="tree_tools/tree_buttons" unique_id=1744382439] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Sets tree presentation mode." +theme_override_styles/normal = SubResource("StyleBoxEmpty_jh28t") +flat = false + +[node name="HSeparator" type="HSeparator" parent="." unique_id=1416084348] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/separation = 0 + +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=89354949] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="status_bar" type="HFlowContainer" parent="MarginContainer" unique_id=1576624226] +layout_direction = 2 +layout_mode = 2 +size_flags_vertical = 2 + +[node name="icon_template" type="SpinBox" parent="MarginContainer/status_bar" unique_id=1487498266] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="ERROR" type="HBoxContainer" parent="MarginContainer/status_bar" unique_id=310880909] +layout_mode = 2 +tooltip_text = "Detected Errors" + +[node name="value" type="LineEdit" parent="MarginContainer/status_bar/ERROR" unique_id=1630464164] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +tooltip_text = "Detected Errors" +text = "0" +alignment = 2 +max_length = 3 +editable = false +context_menu_enabled = false +emoji_menu_enabled = false +shortcut_keys_enabled = false +middle_mouse_paste_enabled = false +selecting_enabled = false +deselect_on_focus_loss_enabled = false +drag_and_drop_selection_enabled = false +virtual_keyboard_enabled = false + +[node name="icon" type="TextureRect" parent="MarginContainer/status_bar/ERROR/value" unique_id=729348405] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 4.0 +offset_top = -8.0 +offset_right = 20.0 +offset_bottom = 8.0 +grow_vertical = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +stretch_mode = 2 + +[node name="buttons" type="VBoxContainer" parent="MarginContainer/status_bar/ERROR" unique_id=541942806] +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="up" type="TextureButton" parent="MarginContainer/status_bar/ERROR/buttons" unique_id=1146002542] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 10 + +[node name="down" type="TextureButton" parent="MarginContainer/status_bar/ERROR/buttons" unique_id=1867410350] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 2 +flip_v = true + +[node name="FAILED" type="HBoxContainer" parent="MarginContainer/status_bar" unique_id=1390016421] +layout_mode = 2 +tooltip_text = "Failed Tests" + +[node name="value" type="LineEdit" parent="MarginContainer/status_bar/FAILED" unique_id=75554621] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +tooltip_text = "Failed Tests" +text = "0" +alignment = 2 +max_length = 3 +editable = false +context_menu_enabled = false +emoji_menu_enabled = false +shortcut_keys_enabled = false +middle_mouse_paste_enabled = false +selecting_enabled = false +deselect_on_focus_loss_enabled = false +drag_and_drop_selection_enabled = false +virtual_keyboard_enabled = false + +[node name="icon" type="TextureRect" parent="MarginContainer/status_bar/FAILED/value" unique_id=1889621378] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 4.0 +offset_top = -8.0 +offset_right = 20.0 +offset_bottom = 8.0 +grow_vertical = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +stretch_mode = 2 + +[node name="buttons" type="VBoxContainer" parent="MarginContainer/status_bar/FAILED" unique_id=699611313] +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="up" type="TextureButton" parent="MarginContainer/status_bar/FAILED/buttons" unique_id=1774403865] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 10 + +[node name="down" type="TextureButton" parent="MarginContainer/status_bar/FAILED/buttons" unique_id=1605438045] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 2 +flip_v = true + +[node name="FLAKY" type="HBoxContainer" parent="MarginContainer/status_bar" unique_id=480870974] +layout_mode = 2 +tooltip_text = "Flaky Tests" + +[node name="value" type="LineEdit" parent="MarginContainer/status_bar/FLAKY" unique_id=2059394293] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +tooltip_text = "Flaky Tests" +text = "0" +alignment = 2 +max_length = 3 +editable = false +context_menu_enabled = false +emoji_menu_enabled = false +shortcut_keys_enabled = false +middle_mouse_paste_enabled = false +selecting_enabled = false +deselect_on_focus_loss_enabled = false +drag_and_drop_selection_enabled = false +virtual_keyboard_enabled = false + +[node name="icon" type="TextureRect" parent="MarginContainer/status_bar/FLAKY/value" unique_id=885817973] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 4.0 +offset_top = -8.0 +offset_right = 20.0 +offset_bottom = 8.0 +grow_vertical = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +stretch_mode = 2 + +[node name="buttons" type="VBoxContainer" parent="MarginContainer/status_bar/FLAKY" unique_id=968503310] +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="up" type="TextureButton" parent="MarginContainer/status_bar/FLAKY/buttons" unique_id=1127861257] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 10 + +[node name="down" type="TextureButton" parent="MarginContainer/status_bar/FLAKY/buttons" unique_id=949128796] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 2 +flip_v = true + +[node name="SKIPPED" type="HBoxContainer" parent="MarginContainer/status_bar" unique_id=2053554589] +layout_mode = 2 +tooltip_text = "Skipped Tests" + +[node name="value" type="LineEdit" parent="MarginContainer/status_bar/SKIPPED" unique_id=1877800941] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +tooltip_text = "Skipped Tests" +text = "0" +alignment = 2 +max_length = 3 +editable = false +context_menu_enabled = false +emoji_menu_enabled = false +shortcut_keys_enabled = false +middle_mouse_paste_enabled = false +selecting_enabled = false +deselect_on_focus_loss_enabled = false +drag_and_drop_selection_enabled = false +virtual_keyboard_enabled = false + +[node name="icon" type="TextureRect" parent="MarginContainer/status_bar/SKIPPED/value" unique_id=932936779] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 4.0 +offset_top = -8.0 +offset_right = 20.0 +offset_bottom = 8.0 +grow_vertical = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +stretch_mode = 2 + +[node name="buttons" type="VBoxContainer" parent="MarginContainer/status_bar/SKIPPED" unique_id=1650258492] +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="up" type="TextureButton" parent="MarginContainer/status_bar/SKIPPED/buttons" unique_id=357027687] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 10 + +[node name="down" type="TextureButton" parent="MarginContainer/status_bar/SKIPPED/buttons" unique_id=603266459] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 2 +flip_v = true + +[node name="ORPHAN" type="HBoxContainer" parent="MarginContainer/status_bar" unique_id=1284157443] +layout_mode = 2 +tooltip_text = "Detected Orphan Nodes" + +[node name="value" type="LineEdit" parent="MarginContainer/status_bar/ORPHAN" unique_id=629163801] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +tooltip_text = "Detected Orphan Nodes" +text = "0" +alignment = 2 +max_length = 3 +editable = false +context_menu_enabled = false +emoji_menu_enabled = false +shortcut_keys_enabled = false +middle_mouse_paste_enabled = false +selecting_enabled = false +deselect_on_focus_loss_enabled = false +drag_and_drop_selection_enabled = false +virtual_keyboard_enabled = false + +[node name="icon" type="TextureRect" parent="MarginContainer/status_bar/ORPHAN/value" unique_id=2121368482] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 4.0 +offset_top = -8.0 +offset_right = 20.0 +offset_bottom = 8.0 +grow_vertical = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +stretch_mode = 2 + +[node name="buttons" type="VBoxContainer" parent="MarginContainer/status_bar/ORPHAN" unique_id=433516040] +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="up" type="TextureButton" parent="MarginContainer/status_bar/ORPHAN/buttons" unique_id=159317457] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 10 + +[node name="down" type="TextureButton" parent="MarginContainer/status_bar/ORPHAN/buttons" unique_id=1357830825] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 2 +flip_v = true + +[connection signal="pressed" from="tree_tools/tree_buttons/btn_tree_sync" to="." method="_on_btn_tree_sync_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.gd new file mode 100644 index 0000000..ffa6526 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.gd @@ -0,0 +1,104 @@ +@tool +extends Control + + +const GdUnitInspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd") + +@onready var _version_label: Control = %version +@onready var _button_wiki: Button = %help +@onready var _tool_button: Button = %tool +@onready var _button_run_overall: Button = %run_overall +@onready var _button_run: Button = %run +@onready var _button_run_debug: Button = %debug +@onready var _button_stop: Button = %stop + + +var inspector: GdUnitInspectorTreeMainPanel +var command_handler: GdUnitCommandHandler + + +func _ready() -> void: + command_handler = GdUnitCommandHandler.instance() + inspector = get_parent().get_parent().find_child("MainPanel", false, false) + if inspector == null: + push_error("Internal error, can't connect to the test inspector!") + else: + inspector.tree_item_selected.connect(_on_inspector_selected) + + GdUnit4Version.init_version_label(_version_label) + + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) + _init_buttons() + + +func _init_buttons() -> void: + _init_button(_button_run_overall, GdUnitCommandRunTestsOverall.ID) + _init_button(_button_run, GdUnitCommandInspectorRunTests.ID) + _init_button(_button_run_debug, GdUnitCommandInspectorDebugTests.ID) + _init_button(_button_stop, GdUnitCommandStopTestSession.ID) + + _button_stop.icon = command_handler.command_icon(GdUnitCommandStopTestSession.ID) + _tool_button.icon = GdUnitUiTools.get_icon("Tools") + _button_wiki.icon = GdUnitUiTools.get_icon("HelpSearch") + # Set run buttons initial disabled + _button_run.disabled = true + _button_run_debug.disabled = true + + +func _init_button(button: Button, comand_id: String) -> void: + button.set_meta("GdUnitCommand", comand_id) + button.icon = command_handler.command_icon(comand_id) + button.shortcut = command_handler.command_shortcut(comand_id) + if button == _button_run_overall: + button.visible = GdUnitSettings.is_inspector_toolbar_button_show() + + +func _on_inspector_selected(item: TreeItem) -> void: + var button_disabled := item == null + _button_run.disabled = button_disabled + _button_run_debug.disabled = button_disabled + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + if event.type() == GdUnitEvent.SESSION_START: + _button_run_overall.disabled = true + _button_run.disabled = true + _button_run_debug.disabled = true + _button_stop.disabled = false + return + if event.type() == GdUnitEvent.SESSION_CLOSE: + _button_run_overall.disabled = false + _button_stop.disabled = true + + +func _on_button_pressed(source: BaseButton) -> void: + var command_id: String = source.get_meta("GdUnitCommand") + await command_handler.command_execute(command_id) + + +func _on_wiki_pressed() -> void: + var status := OS.shell_open("https://godot-gdunit-labs.github.io/gdUnit4/latest") + if status != OK: + push_error("Can't open GdUnit4 documentaion page: %s" % error_string(status)) + + +func _on_btn_tool_pressed() -> void: + var settings_dlg: Window = EditorInterface.get_base_control().find_child("GdUnitSettingsDialog", false, false) + if settings_dlg == null: + settings_dlg = preload("res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn").instantiate() + EditorInterface.get_base_control().add_child(settings_dlg, true) + settings_dlg.popup_centered_ratio(.60) + + +func _on_settings_changed(property: GdUnitProperty) -> void: + # needs to wait a frame to be command handler notified first for settings changes + await get_tree().process_frame + + _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() + + if property.name().begins_with(GdUnitSettings.GROUP_SHORTCUT_INSPECTOR): + _button_run.shortcut = command_handler.command_shortcut(GdUnitCommandInspectorRunTests.ID) + _button_run_debug.shortcut = command_handler.command_shortcut(GdUnitCommandInspectorDebugTests.ID) + _button_run_overall.shortcut = command_handler.command_shortcut(GdUnitCommandRunTestsOverall.ID) + _button_stop.shortcut = command_handler.command_shortcut(GdUnitCommandStopTestSession.ID) diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.gd.uid b/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.gd.uid new file mode 100644 index 0000000..9584f9d --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.gd.uid @@ -0,0 +1 @@ +uid://murta7xrtwlk diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.tscn b/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.tscn new file mode 100644 index 0000000..5ce9aa2 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.tscn @@ -0,0 +1,150 @@ +[gd_scene format=3 uid="uid://dx7xy4dgi3wwb"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.gd" id="3"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_c7rhl"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3erui"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_p22nw"] + +[sub_resource type="InputEventKey" id="InputEventKey_4j3on"] +alt_pressed = true +pressed = true +keycode = 4194338 +physical_keycode = 4194338 + +[sub_resource type="Shortcut" id="Shortcut_5pa04"] +events = [SubResource("InputEventKey_4j3on")] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3lcek"] + +[sub_resource type="InputEventKey" id="InputEventKey_85ykl"] +alt_pressed = true +pressed = true +keycode = 4194336 +physical_keycode = 4194336 + +[sub_resource type="Shortcut" id="Shortcut_fpubx"] +events = [SubResource("InputEventKey_85ykl")] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ndw0i"] + +[sub_resource type="InputEventKey" id="InputEventKey_1iti7"] +alt_pressed = true +pressed = true +keycode = 4194337 +physical_keycode = 4194337 + +[sub_resource type="Shortcut" id="Shortcut_hpi24"] +events = [SubResource("InputEventKey_1iti7")] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_eoihf"] + +[sub_resource type="InputEventKey" id="InputEventKey_hb3do"] +alt_pressed = true +pressed = true +keycode = 4194339 +physical_keycode = 4194339 + +[sub_resource type="Shortcut" id="Shortcut_lm2q6"] +events = [SubResource("InputEventKey_hb3do")] + +[node name="ToolBar" type="HBoxContainer" unique_id=295615556] +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 32.0 +grow_horizontal = 2 +script = ExtResource("3") + +[node name="tools" type="HBoxContainer" parent="." unique_id=753222103] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 + +[node name="help" type="Button" parent="tools" unique_id=1834372497] +unique_name_in_owner = true +layout_mode = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_c7rhl") + +[node name="tool" type="Button" parent="tools" unique_id=144277615] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "GdUnit Settings" +theme_override_styles/normal = SubResource("StyleBoxEmpty_3erui") + +[node name="controls" type="HBoxContainer" parent="." unique_id=108317981] +layout_mode = 2 +size_flags_horizontal = 6 +size_flags_vertical = 4 +alignment = 1 + +[node name="VSeparator3" type="VSeparator" parent="controls" unique_id=1244151035] +layout_mode = 2 + +[node name="run_overall" type="Button" parent="controls" unique_id=95775836] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Run overall tests" +theme_override_styles/normal = SubResource("StyleBoxEmpty_p22nw") +shortcut = SubResource("Shortcut_5pa04") +metadata/GdUnitCommand = "Run Tests Overall" + +[node name="run" type="Button" parent="controls" unique_id=121281977] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Rerun unit tests" +theme_override_styles/normal = SubResource("StyleBoxEmpty_3lcek") +disabled = true +shortcut = SubResource("Shortcut_fpubx") +metadata/GdUnitCommand = "Run Inspector Tests" + +[node name="debug" type="Button" parent="controls" unique_id=1188465777] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Rerun unit tests (Debug)" +theme_override_styles/normal = SubResource("StyleBoxEmpty_ndw0i") +disabled = true +shortcut = SubResource("Shortcut_hpi24") +metadata/GdUnitCommand = "Debug Inspector Tests" + +[node name="stop" type="Button" parent="controls" unique_id=2086070257] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Stops runing unit tests" +theme_override_styles/normal = SubResource("StyleBoxEmpty_eoihf") +disabled = true +shortcut = SubResource("Shortcut_lm2q6") +metadata/GdUnitCommand = "Stop Test Session" + +[node name="VSeparator4" type="VSeparator" parent="controls" unique_id=1661024234] +layout_mode = 2 + +[node name="CenterContainer" type="HBoxContainer" parent="." unique_id=1521483443] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +alignment = 2 + +[node name="version" type="Label" parent="CenterContainer" unique_id=558200171] +unique_name_in_owner = true +auto_translate_mode = 2 +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 2 +size_flags_vertical = 13 +localize_numeral_system = false +horizontal_alignment = 1 +justification_flags = 160 + +[connection signal="pressed" from="tools/help" to="." method="_on_wiki_pressed"] +[connection signal="pressed" from="tools/tool" to="." method="_on_btn_tool_pressed"] +[connection signal="pressed" from="controls/run_overall" to="." method="_on_button_pressed" flags=18] +[connection signal="pressed" from="controls/run" to="." method="_on_button_pressed" flags=18] +[connection signal="pressed" from="controls/debug" to="." method="_on_button_pressed" flags=18] +[connection signal="pressed" from="controls/stop" to="." method="_on_button_pressed" flags=18] diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd new file mode 100644 index 0000000..3ea4b3e --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd @@ -0,0 +1,1141 @@ +@tool +extends Control + +## Will be emitted when the test index counter is changed +signal test_counters_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE) +signal tree_item_selected(item: TreeItem) + + +@onready var _tree: Tree = %Tree +@onready var _report_panel: GdUnitReportPanel = %report +@onready var _context_menu: GdUnitInspectorContextMenu = %contextMenu +@onready var _discover_hint: Control = %discover_hint +@onready var _spinner: Button = %spinner + +# loading tree icons +var ICON_SPINNER: Texture2D +var ICON_GD_SCRIPT: Texture2D +var ICON_CS_SCRIPT: Texture2D +var ICON_FOLDER: Texture2D + + +enum GdUnitType { + FOLDER, + TEST_SUITE, + TEST_CASE, + TEST_GROUP +} + +const META_GDUNIT_PROGRESS_COUNT_MAX := "gdUnit_progress_count_max" +const META_GDUNIT_PROGRESS_INDEX := "gdUnit_progress_index" +const META_TEST_CASE := "gdunit_test_case" +const META_GDUNIT_NAME := "gdUnit_name" +const META_GDUNIT_STATE := "gdUnit_state" +const META_GDUNIT_TYPE := "gdUnit_type" +const META_GDUNIT_SUCCESS_TESTS := "gdUnit_suite_success_tests" +const META_GDUNIT_REPORT := "gdUnit_report" +const META_GDUNIT_ORPHAN := "gdUnit_orphan" +const META_GDUNIT_EXECUTION_TIME := "gdUnit_execution_time" +const META_GDUNIT_ORIGINAL_INDEX = "gdunit_original_index" +const STATE = GdUnitInspectorTreeConstants.STATE + + +var _tree_root: TreeItem +var _current_selected_item: TreeItem = null +var _current_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode() +var _run_test_recovery := true + + +## Used for debugging purposes only +func print_tree_item_ids(parent: TreeItem) -> TreeItem: + for child in parent.get_children(): + if child.has_meta(META_TEST_CASE): + var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE) + prints(test_case.guid, test_case.test_name) + + if child.get_child_count() > 0: + print_tree_item_ids(child) + + return null + + +func _find_tree_item(parent: TreeItem, item_name: String) -> TreeItem: + for child in parent.get_children(): + if child.get_meta(META_GDUNIT_NAME) == item_name: + return child + return null + + +func _find_tree_item_by_id(parent: TreeItem, id: GdUnitGUID) -> TreeItem: + for child in parent.get_children(): + if is_test_id(child, id): + return child + if child.get_child_count() > 0: + var item := _find_tree_item_by_id(child, id) + if item != null: + return item + + return null + + +func _find_tree_item_by_test_suite(parent: TreeItem, suite_path: String, suite_name: String) -> TreeItem: + for child in parent.get_children(): + if child.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE: + var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE) + if test_case.suite_resource_path == suite_path and test_case.suite_name == suite_name: + return child + if child.get_child_count() > 0: + var item := _find_tree_item_by_test_suite(child, suite_path, suite_name) + if item != null: + return item + return null + + +func _find_first_item_by_state(parent: TreeItem, item_state: STATE, reverse := false) -> TreeItem: + var itmes := parent.get_children() + if reverse: + itmes.reverse() + for item in itmes: + if is_test_case(item) and (is_item_state(item, item_state)): + return item + var failure_item := _find_first_item_by_state(item, item_state, reverse) + if failure_item != null: + return failure_item + return null + + +func _find_last_item_by_state(parent: TreeItem, item_state: STATE) -> TreeItem: + return _find_first_item_by_state(parent, item_state, true) + + +func _find_item_by_state(current: TreeItem, item_state: STATE, prev := false) -> TreeItem: + var next := current.get_prev_in_tree() if prev else current.get_next_in_tree() + if next == null or next == _tree_root: + return null + if is_test_case(next) and is_item_state(next, item_state): + return next + return _find_item_by_state(next, item_state, prev) + + +func is_item_state(item: TreeItem, item_state: STATE) -> bool: + return get_item_state(item).has(item_state) + + +func is_state_running(item: TreeItem) -> bool: + return is_item_state(item, STATE.RUNNING) + + +func is_state_success(item: TreeItem) -> bool: + return is_item_state(item, STATE.SUCCESS) + + +func is_state_warning(item: TreeItem) -> bool: + return is_item_state(item, STATE.WARNING) + + +func is_state_failed(item: TreeItem) -> bool: + return is_item_state(item, STATE.FAILED) + + +func is_state_error(item: TreeItem) -> bool: + return is_item_state(item, STATE.ERROR) or is_item_state(item, STATE.ABORDED) + + +func is_item_state_orphan(item: TreeItem) -> bool: + return item.has_meta(META_GDUNIT_ORPHAN) + + +func is_test_suite(item: TreeItem) -> bool: + return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE + + +func is_test_case(item: TreeItem) -> bool: + return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_CASE + + +func is_folder(item: TreeItem) -> bool: + return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER + + +func is_test_id(item: TreeItem, id: GdUnitGUID) -> bool: + if not item.has_meta(META_TEST_CASE): + return false + + var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE) + return test_case.guid.equals(id) + + +func disable_test_recovery() -> void: + _run_test_recovery = false + + +@warning_ignore("return_value_discarded") +func _ready() -> void: + if Engine.is_editor_hint(): + var base_control := EditorInterface.get_base_control() + base_control.set_meta("GdUnit4Inspector", self) + + _init_icons() + init_tree() + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_test_discover_added.connect(on_test_case_discover_added) + GdUnitSignals.instance().gdunit_test_discover_deleted.connect(on_test_case_discover_deleted) + GdUnitSignals.instance().gdunit_test_discover_modified.connect(on_test_case_discover_modified) + if _run_test_recovery: + GdUnitTestDiscoverer.restore_last_session() + + +func _notification(what: int) -> void: + if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: + _init_icons() + + +func _init_icons() -> void: + _spinner.icon = GdUnitUiTools.get_spinner() + ICON_GD_SCRIPT = GdUnitUiTools.get_icon("GDScript", GdUnitEditorColorTheme.state_initial) + ICON_CS_SCRIPT = GdUnitUiTools.get_icon("CSharpScript", GdUnitEditorColorTheme.state_initial) + ICON_FOLDER = GdUnitUiTools.get_icon("Folder", GdUnitEditorColorTheme.folder_color) + + +# we need current to manually redraw bacause of the animation bug +# https://github.com/godotengine/godot/issues/69330 +func _process(_delta: float) -> void: + if is_visible_in_tree(): + queue_redraw() + + +func init_tree() -> void: + cleanup_tree() + _tree.deselect_all() + _tree.set_hide_root(true) + _tree.ensure_cursor_is_visible() + _tree.set_allow_reselect(true) + _tree.set_allow_rmb_select(true) + _tree.set_columns(2) + _tree.set_column_clip_content(0, true) + _tree.set_column_expand_ratio(0, 1) + _tree.set_column_custom_minimum_width(0, 240) + _tree.set_column_expand_ratio(1, 0) + _tree.set_column_custom_minimum_width(1, 100) + _tree_root = _tree.create_item() + _tree_root.set_text(0, "tree_root") + _tree_root.set_meta(META_GDUNIT_NAME, "tree_root") + _tree_root.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0) + _tree_root.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) + set_item_state(_tree_root, STATE.INITIAL) + # fix tree icon scaling + var scale_factor := EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0 + _tree.set("theme_override_constants/icon_max_width", 16 * scale_factor) + + +func cleanup_tree() -> void: + _report_panel.clear() + if not _tree_root: + return + _free_recursive() + _tree.clear() + _current_selected_item = null + + +func _free_recursive(items:=_tree_root.get_children()) -> void: + for item in items: + _free_recursive(item.get_children()) + item.call_deferred("free") + + +func sort_tree_items(parent: TreeItem) -> void: + _sort_tree_items(parent, GdUnitSettings.get_inspector_tree_sort_mode()) + _tree.queue_redraw() + + +static func _sort_tree_items(parent: TreeItem, sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void: + parent.visible = false + var items := parent.get_children() + # first remove all childs before sorting + for item in items: + parent.remove_child(item) + + # do sort by selected sort mode + match sort_mode: + GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED: + items.sort_custom(sort_items_by_original_index) + + GdUnitInspectorTreeConstants.SORT_MODE.NAME_ASCENDING: + items.sort_custom(sort_items_by_name.bind(true)) + + GdUnitInspectorTreeConstants.SORT_MODE.NAME_DESCENDING: + items.sort_custom(sort_items_by_name.bind(false)) + + GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME: + items.sort_custom(sort_items_by_execution_time) + + # readding sorted childs + for item in items: + parent.add_child(item) + if item.get_child_count() > 0: + _sort_tree_items(item, sort_mode) + parent.visible = true + + +static func sort_items_by_name(a: TreeItem, b: TreeItem, ascending: bool) -> bool: + var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE) + var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE) + + # Sort folders to the top + if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER: + return true + if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER: + return false + + # sort by name + var name_a: String = a.get_meta(META_GDUNIT_NAME) + var name_b: String = b.get_meta(META_GDUNIT_NAME) + var comparison := name_a.naturalnocasecmp_to(name_b) + + return comparison < 0 if ascending else comparison > 0 + + +static func sort_items_by_execution_time(a: TreeItem, b: TreeItem) -> bool: + var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE) + var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE) + + # Sort folders to the top + if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER: + return true + if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER: + return false + + var execution_time_a :int = a.get_meta(META_GDUNIT_EXECUTION_TIME) + var execution_time_b :int = b.get_meta(META_GDUNIT_EXECUTION_TIME) + # if has same execution time sort by name + if execution_time_a == execution_time_b: + var name_a :String = a.get_meta(META_GDUNIT_NAME) + var name_b :String = b.get_meta(META_GDUNIT_NAME) + return name_a.naturalnocasecmp_to(name_b) > 0 + return execution_time_a > execution_time_b + + +static func sort_items_by_original_index(a: TreeItem, b: TreeItem) -> bool: + var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE) + var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE) + + # Sort folders to the top + if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER: + return true + if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER: + return false + + var index_a :int = a.get_meta(META_GDUNIT_ORIGINAL_INDEX) + var index_b :int = b.get_meta(META_GDUNIT_ORIGINAL_INDEX) + + # Sorting by index + return index_a < index_b + + +func restructure_tree(parent: TreeItem, tree_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void: + _current_tree_view_mode = tree_mode + + match tree_mode: + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT: + restructure_tree_to_flat(parent) + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE: + restructure_tree_to_tree(parent) + recalculate_counters(_tree_root) + # finally apply actual sort mode + sort_tree_items(_tree_root) + + +# Restructure into flat mode +func restructure_tree_to_flat(parent: TreeItem) -> void: + var folders := flatmap_folders(parent) + # Store current folder paths and their test suites + for folder_path: String in folders: + var test_suites: Array[TreeItem] = folders[folder_path] + if test_suites.is_empty(): + continue + + # Create flat folder and move test suites into it + var folder := _tree.create_item(parent) + folder.set_meta(META_GDUNIT_NAME, folder_path) + update_item_total_counter(folder) + set_state_initial(folder, GdUnitType.FOLDER) + + # Move test suites under the flat folder + for test_suite in test_suites: + var old_parent := test_suite.get_parent() + old_parent.remove_child(test_suite) + folder.add_child(test_suite) + + # Cleanup old folder structure + cleanup_empty_folders(parent) + + +# Restructure into hierarchical tree mode +func restructure_tree_to_tree(parent: TreeItem) -> void: + var items_to_process := parent.get_children().duplicate() + + for item: TreeItem in items_to_process: + if is_folder(item): + var folder_path: String = item.get_meta(META_GDUNIT_NAME) + var parts := folder_path.split("/") + + if parts.size() > 1: + var current_parent := parent + # Build folder hierarchy + for part in parts: + var next := _find_tree_item(current_parent, part) + if not next: + next = _tree.create_item(current_parent) + next.set_meta(META_GDUNIT_NAME, part) + set_state_initial(next, GdUnitType.FOLDER) + current_parent = next + + # Move test suites to deepest folder + var test_suites := item.get_children() + for test_suite in test_suites: + item.remove_child(test_suite) + current_parent.add_child(test_suite) + + # Remove the flat folder + item.get_parent().remove_child(item) + item.free() + + +func flatmap_folders(parent: TreeItem) -> Dictionary: + var folder_map := {} + + for item in parent.get_children(): + if is_folder(item): + var current_path: String = item.get_meta(META_GDUNIT_NAME) + # Get parent folder paths + var parent_path := get_parent_folder_path(item) + if parent_path: + current_path = parent_path + "/" + current_path + + # Collect direct children of this folder + var children: Array[TreeItem] = [] + for child in item.get_children(): + if is_test_suite(child): + children.append(child) + + # Add children to existing path or create new entry + if not children.is_empty(): + if folder_map.has(current_path): + @warning_ignore("unsafe_method_access") + folder_map[current_path].append_array(children) + else: + folder_map[current_path] = children + + # Recursively process subfolders + var sub_folders := flatmap_folders(item) + for path: String in sub_folders.keys(): + if folder_map.has(path): + @warning_ignore("unsafe_method_access") + folder_map[path].append_array(sub_folders[path]) + else: + folder_map[path] = sub_folders[path] + return folder_map + + +func get_parent_folder_path(item: TreeItem) -> String: + var path := "" + var parent := item.get_parent() + + while parent != _tree_root: + if is_folder(parent): + path = parent.get_meta(META_GDUNIT_NAME) + ("/" + path if path else "") + parent = parent.get_parent() + + return path + + +func cleanup_empty_folders(parent: TreeItem) -> void: + var folders: Array[TreeItem] = [] + # First collect all folders to avoid modification during iteration + for item in parent.get_children(): + if is_folder(item): + folders.append(item) + + # Process collected folders + for folder in folders: + cleanup_empty_folders(folder) + # Remove folder if it has no children after cleanup + if folder.get_child_count() == 0: + parent.remove_child(folder) + folder.free() + + +func reset_tree_state(parent: TreeItem) -> void: + if parent == _tree_root: + _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0) + set_item_state(_tree_root, STATE.INITIAL) + test_counters_changed.emit(0, 0, STATE.INITIAL) + + for item in parent.get_children(): + set_state_initial(item, get_item_type(item)) + reset_tree_state(item) + + +func select_item(item: TreeItem) -> TreeItem: + if item != null: + # enshure the parent is collapsed + do_collapse_parent(item) + item.select(0) + _tree.ensure_cursor_is_visible() + _tree.scroll_to_item(item, true) + return item + + +func do_collapse_parent(item: TreeItem) -> void: + if item != null: + item.collapsed = false + do_collapse_parent(item.get_parent()) + + +func do_collapse_all(collapse: bool, parent := _tree_root) -> void: + for item in parent.get_children(): + item.collapsed = collapse + if not collapse: + do_collapse_all(collapse, item) + + +func set_state_initial(item: TreeItem, type: GdUnitType) -> void: + item.set_text(0, str(item.get_meta(META_GDUNIT_NAME))) + item.set_custom_color(0, GdUnitEditorColorTheme.state_initial) + item.set_tooltip_text(0, "") + item.set_text_overrun_behavior(0, TextServer.OVERRUN_TRIM_CHAR) + item.set_expand_right(0, true) + + item.set_custom_color(1, GdUnitEditorColorTheme.state_initial) + item.set_text(1, "") + item.set_expand_right(1, true) + item.set_tooltip_text(1, "") + item.set_meta(META_GDUNIT_TYPE, type) + item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) + item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) and item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) > 0: + item.set_text(0, "(0/%d) %s" % [item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]) + item.remove_meta(META_GDUNIT_REPORT) + item.remove_meta(META_GDUNIT_ORPHAN) + + set_item_state(item, STATE.INITIAL) + + +func set_state_running(item: TreeItem) -> void: + if is_state_running(item): + return + if is_item_state(item, STATE.INITIAL): + item.set_custom_color(0, GdUnitEditorColorTheme.state_success) + item.set_custom_color(1, GdUnitEditorColorTheme.state_success) + set_item_state(item, STATE.RUNNING) + item.collapsed = false + + var parent := item.get_parent() + if parent != _tree_root: + set_state_running(parent) + + +func set_state_succeded(item: TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item) or is_state_failed(item): + return + if item == _tree_root: + return + item.set_custom_color(0, GdUnitEditorColorTheme.state_success) + item.collapsed = GdUnitSettings.is_inspector_node_collapse() + set_item_state(item, STATE.SUCCESS) + + +func set_state_flaky(item: TreeItem, event: GdUnitEvent) -> void: + # Do not overwrite higher states + if is_state_error(item): + return + var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT) + if retry_count > 1: + var item_text: String = item.get_meta(META_GDUNIT_NAME) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)] + item.set_text(0, "%s (%s retries)" % [item_text, retry_count]) + item.set_custom_color(0, GdUnitEditorColorTheme.state_flaky) + item.collapsed = false + set_item_state(item, STATE.FLAKY) + + +func set_state_skipped(item: TreeItem) -> void: + item.set_text(1, "(skipped)") + item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT) + item.set_custom_color(0, GdUnitEditorColorTheme.state_skipped) + item.set_custom_color(1, GdUnitEditorColorTheme.state_skipped) + item.collapsed = false + set_item_state(item, STATE.SKIPPED) + + +func set_state_warnings(item: TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item) or is_state_failed(item): + return + item.set_custom_color(0, GdUnitEditorColorTheme.state_warning) + item.collapsed = false + set_item_state(item, STATE.WARNING) + + +func set_state_failed(item: TreeItem, event: GdUnitEvent) -> void: + # Do not overwrite higher states + if is_state_error(item): + return + var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT) + if retry_count > 1: + var item_text: String = item.get_meta(META_GDUNIT_NAME) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)] + item.set_text(0, "%s (%s retries)" % [item_text, retry_count]) + item.set_custom_color(0, GdUnitEditorColorTheme.state_failure) + item.collapsed = false + set_item_state(item, STATE.FAILED) + + +func set_state_error(item: TreeItem) -> void: + item.set_custom_color(0, GdUnitEditorColorTheme.state_error) + set_item_state(item, STATE.ERROR) + item.collapsed = false + + +func set_state_aborted(item: TreeItem) -> void: + item.set_custom_color(0, GdUnitEditorColorTheme.state_error) + item.clear_custom_bg_color(0) + item.set_text(1, "(aborted)") + item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT) + set_item_state(item, STATE.ABORDED) + item.collapsed = false + + +func set_state_orphan(item: TreeItem, event: GdUnitEvent) -> void: + var orphan_count := event.statistic(GdUnitEvent.ORPHAN_NODES) + if orphan_count == 0: + return + if item.has_meta(META_GDUNIT_ORPHAN): + orphan_count += item.get_meta(META_GDUNIT_ORPHAN) + set_item_state(item, STATE.ORPHAN) + item.set_meta(META_GDUNIT_ORPHAN, orphan_count) + item.set_tooltip_text(0, "Total <%d> orphan nodes detected." % orphan_count) + set_item_state(item, STATE.ORPHAN) + + +func update_state(item: TreeItem, event: GdUnitEvent, add_reports := true) -> void: + # we do not show the root + if item == null: + return + + if event.is_skipped(): + set_state_skipped(item) + elif event.is_success() and event.is_flaky(): + set_state_flaky(item, event) + elif event.is_success(): + set_state_succeded(item) + elif event.is_error(): + set_state_error(item) + elif event.is_failed(): + set_state_failed(item, event) + elif event.is_warning(): + set_state_warnings(item) + if add_reports: + for report in event.reports(): + add_report(item, report) + set_state_orphan(item, event) + + var parent := item.get_parent() + if parent == null: + return + + var item_state: int = get_item_state(item)[0] + var parent_state: int = get_item_state(parent)[0] + if item_state <= parent_state: + return + update_state(item.get_parent(), event, false) + + +func add_report(item: TreeItem, report: GdUnitReport) -> void: + var reports: Array[GdUnitReport] = [] + if item.has_meta(META_GDUNIT_REPORT): + reports = get_item_reports(item) + reports.append(report) + item.set_meta(META_GDUNIT_REPORT, reports) + + +func abort_running(items := _tree_root.get_children()) -> void: + for item in items: + if is_state_running(item): + set_state_aborted(item) + abort_running(item.get_children()) + + +func _on_select_next_item_by_state(item_state: int) -> TreeItem: + var current_selected := _tree.get_selected() + # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found + current_selected = _find_first_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state) + # If no next failure found, then we try to select first + if current_selected == null: + current_selected = _find_first_item_by_state(_tree_root, item_state) + return select_item(current_selected) + + +func _on_select_previous_item_by_state(item_state: int) -> TreeItem: + var current_selected := _tree.get_selected() + # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found + current_selected = _find_last_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state, true) + # If no next failure found, then we try to select first last + if current_selected == null: + current_selected = _find_last_item_by_state(_tree_root, item_state) + return select_item(current_selected) + + +func select_first_orphan() -> void: + for parent in _tree_root.get_children(): + if not is_state_success(parent): + for item in parent.get_children(): + if is_item_state_orphan(item): + parent.set_collapsed(false) + @warning_ignore("return_value_discarded") + select_item(item) + return + + +func update_test_suite(event: GdUnitEvent) -> void: + var item := _find_tree_item_by_test_suite(_tree_root, event.resource_path(), event.suite_name()) + if not item: + push_error("[InspectorTreeMainPanel#update_test_suite] Internal Error: Can't find test suite item '{_suite_name}' for {_resource_path} ".format(event)) + return + + update_item_elapsed_time_counter(item, event.elapsed_time()) + update_state(item, event) + + +func update_test_case(event: GdUnitEvent) -> void: + var item := _find_tree_item_by_id(_tree_root, event.guid()) + if not item: + #push_error("Internal Error: Can't find test id %s" % [event.guid()]) + return + if event.type() == GdUnitEvent.TESTCASE_BEFORE: + set_state_running(item) + # force scrolling to current test case + _tree.scroll_to_item(item, true) + return + + if event.type() == GdUnitEvent.TESTCASE_AFTER: + update_item_elapsed_time_counter(item, event.elapsed_time()) + if event.is_success() or event.is_warning(): + update_item_processed_counter(item) + update_state(item, event) + update_progress_counters(item) + + +func create_item(parent: TreeItem, test: GdUnitTestCase, item_name: String, type: GdUnitType) -> TreeItem: + var item := _tree.create_item(parent) + item.collapsed = true + item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index()) + item.set_text(0, item_name) + match type: + GdUnitType.TEST_CASE: + item.set_meta(META_TEST_CASE, test) + GdUnitType.TEST_GROUP: + # We need to create a copy of the test record meta with a new uniqe guid + item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.test_name)) + GdUnitType.TEST_SUITE: + # We need to create a copy of the test record meta with a new uniqe guid + item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.suite_name)) + + item.set_meta(META_GDUNIT_NAME, item_name) + set_state_initial(item, type) + update_item_total_counter(item) + return item + + +static func get_item_state(item: TreeItem) -> Array[STATE]: + if item.has_meta(META_GDUNIT_STATE): + return item.get_meta(META_GDUNIT_STATE) + return [STATE.INITIAL, STATE.INITIAL] + + +func set_item_state(item: TreeItem, state: STATE) -> void: + if item == _tree_root: + return + + if state != STATE.RUNNING and (is_test_suite(item) or is_folder(item)): + var resource_path := get_item_source_file(item) + item.set_icon(0, get_icon_by_file_type(resource_path)) + else: + item.set_icon(0, GdUnitUiTools.get_state_icon(state)) + + if state == STATE.INITIAL: + var inital_state: Array[STATE] = [STATE.INITIAL, STATE.INITIAL] + item.set_meta(META_GDUNIT_STATE, inital_state) + elif state == STATE.ORPHAN: + get_item_state(item)[1] = state + else: + get_item_state(item)[0] = state + + +func update_item_total_counter(item: TreeItem) -> void: + if item == null: + return + + var child_count := get_total_child_count(item) + if child_count > 0: + item.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, child_count) + item.set_text(0, "(0/%d) %s" % [child_count, item.get_meta(META_GDUNIT_NAME)]) + + update_item_total_counter(item.get_parent()) + + +func get_total_child_count(item: TreeItem) -> int: + var total_count := 0 + for child in item.get_children(): + total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) else 1 + return total_count + + +func update_item_processed_counter(item: TreeItem, add_count := 1) -> void: + if item == _tree_root: + return + + var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + add_count + item.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + item.set_text(0, "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]) + + update_item_processed_counter(item.get_parent(), add_count) + + +func update_progress_counters(item: TreeItem) -> void: + var index: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_INDEX) + 1 + var total_test: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) + var state := get_item_state(item)[0] + test_counters_changed.emit(index, total_test, state) + _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, index) + + +func recalculate_counters(parent: TreeItem) -> void: + # Reset the counter first + if parent.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + if parent.has_meta(META_GDUNIT_PROGRESS_INDEX): + parent.set_meta(META_GDUNIT_PROGRESS_INDEX, 0) + if parent.has_meta(META_GDUNIT_SUCCESS_TESTS): + parent.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) + + # Calculate new count based on children + var total_count := 0 + var success_count := 0 + var progress_index := 0 + + for child in parent.get_children(): + if child.get_child_count() > 0: + # Recursively update child counters first + recalculate_counters(child) + # Add child's counters to parent + if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) + if child.has_meta(META_GDUNIT_SUCCESS_TESTS): + success_count += child.get_meta(META_GDUNIT_SUCCESS_TESTS) + if child.has_meta(META_GDUNIT_PROGRESS_INDEX): + progress_index += child.get_meta(META_GDUNIT_PROGRESS_INDEX) + elif is_test_case(child): + # Count individual test cases + total_count += 1 + # Count completed tests + if is_state_success(child) or is_state_warning(child) or is_state_failed(child) or is_state_error(child): + progress_index += 1 + if is_state_success(child) or is_state_warning(child): + success_count += 1 + + # Update the counters + if total_count > 0: + parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_count) + parent.set_meta(META_GDUNIT_PROGRESS_INDEX, progress_index) + parent.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count) + + # Update the display text + parent.set_text(0, "(%d/%d) %s" % [success_count, total_count, parent.get_meta(META_GDUNIT_NAME)]) + + +func update_item_elapsed_time_counter(item: TreeItem, time: int) -> void: + item.set_text(1, "%s" % LocalTime.elapsed(time)) + item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT) + item.set_meta(META_GDUNIT_EXECUTION_TIME, time) + + var parent := item.get_parent() + if parent == _tree_root: + return + var elapsed_time :int = parent.get_meta(META_GDUNIT_EXECUTION_TIME) + time + var type :GdUnitType = item.get_meta(META_GDUNIT_TYPE) + match type: + GdUnitType.TEST_CASE: + return + GdUnitType.TEST_SUITE: + update_item_elapsed_time_counter(parent, elapsed_time) + #GdUnitType.FOLDER: + # update_item_elapsed_time_counter(parent, elapsed_time) + + +func get_icon_by_file_type(path: String) -> Texture2D: + if path.get_extension() == "gd": + return ICON_GD_SCRIPT + if path.get_extension() == "cs": + return ICON_CS_SCRIPT + return ICON_FOLDER + + +func on_test_case_discover_added(test_case: GdUnitTestCase) -> void: + var test_root_folder := GdUnitSettings.test_root_folder().replace("res://", "") + var fully_qualified_name := test_case.fully_qualified_name.trim_suffix(test_case.display_name) + var parts := fully_qualified_name.split(".", false) + parts.append(test_case.display_name) + # Skip tree structure until test root folder + var index := parts.find(test_root_folder) + if index != -1: + parts = parts.slice(index+1) + + match _current_tree_view_mode: + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT: + create_items_tree_mode_flat(test_case, parts) + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE: + create_items_tree_mode_tree(test_case, parts) + + +func create_items_tree_mode_tree(test_case: GdUnitTestCase, parts: PackedStringArray) -> void: + var parent := _tree_root + var is_suite_assigned := false + var suite_name := test_case.suite_name.split(".")[-1] + for item_name in parts: + var next := _find_tree_item(parent, item_name) + if next != null: + parent = next + continue + + if not is_suite_assigned and suite_name == item_name: + next = create_item(parent, test_case, item_name, GdUnitType.TEST_SUITE) + is_suite_assigned = true + elif item_name == test_case.display_name: + next = create_item(parent, test_case, item_name, GdUnitType.TEST_CASE) + # On grouped tests (parameterized tests) + elif item_name == test_case.test_name: + next = create_item(parent, test_case, item_name, GdUnitType.TEST_GROUP) + else: + next = create_item(parent, test_case, item_name, GdUnitType.FOLDER) + parent = next + + +func create_items_tree_mode_flat(test_case: GdUnitTestCase, parts: PackedStringArray) -> void: + # All parts except the last two (suite name and test name/display name) + var slice_index := -2 if parts[-1] == test_case.test_name else -3 + var path_parts := parts.slice(0, slice_index) + var folder_path := "/".join(path_parts) + + # Find or create flat folder + var folder_item: TreeItem + if folder_path.is_empty(): + folder_item = _tree_root + else: + folder_item = _find_tree_item(_tree_root, folder_path) + if folder_item == null: + folder_item = create_item(_tree_root, test_case, folder_path, GdUnitType.FOLDER) + + # Find suite under the flat folder (second to last part) + var suite_item := _find_tree_item(folder_item, test_case.suite_name) + if suite_item == null: + suite_item = create_item(folder_item, test_case, test_case.suite_name, GdUnitType.TEST_SUITE) + + # Add test case or group under the suite + if test_case.test_name != test_case.display_name: + # It's a parameterized test group + var group_item := _find_tree_item(suite_item, test_case.test_name) + if group_item == null: + group_item = create_item(suite_item, test_case, test_case.test_name, GdUnitType.TEST_GROUP) + create_item(group_item, test_case, test_case.display_name, GdUnitType.TEST_CASE) + else: + create_item(suite_item, test_case, test_case.display_name, GdUnitType.TEST_CASE) + + +func on_test_case_discover_deleted(test_case: GdUnitTestCase) -> void: + var item := _find_tree_item_by_id(_tree_root, test_case.guid) + if item != null: + var parent := item.get_parent() + parent.remove_child(item) + + # update the cached counters + var item_success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + var item_total_test_count: int = item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + var total_test_count: int = parent.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_test_count-item_total_test_count) + + # propagate counter update to all parents + update_item_total_counter(parent) + update_item_processed_counter(parent, -item_success_count) + + +func on_test_case_discover_modified(test_case: GdUnitTestCase) -> void: + var item := _find_tree_item_by_id(_tree_root, test_case.guid) + if item != null: + item.set_meta(META_TEST_CASE, test_case) + item.set_text(0, test_case.display_name) + item.set_meta(META_GDUNIT_NAME, test_case.display_name) + + +func get_item_reports(item: TreeItem) -> Array[GdUnitReport]: + if item == null or not item.has_meta(META_GDUNIT_REPORT): + return [] + return item.get_meta(META_GDUNIT_REPORT) + + +func get_item_test_line_number(item: TreeItem) -> int: + if item == null or not item.has_meta(META_TEST_CASE): + return -1 + + var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE) + return test_case.line_number + + +func get_item_source_file(item: TreeItem) -> String: + if item == null or not item.has_meta(META_TEST_CASE): + return "" + + var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE) + return test_case.source_file + + +func get_item_type(item: TreeItem) -> GdUnitType: + if item == null or not item.has_meta(META_GDUNIT_TYPE): + return GdUnitType.FOLDER + return item.get_meta(META_GDUNIT_TYPE) + + +func _dump_tree_as_json(dump_name: String) -> void: + var dict := _to_json(_tree_root) + var file := FileAccess.open("res://%s.json" % dump_name, FileAccess.WRITE) + file.store_string(JSON.stringify(dict, "\t")) + + +func _to_json(parent :TreeItem) -> Dictionary: + var item_as_dict := GdObjects.obj2dict(parent) + item_as_dict["TreeItem"]["childrens"] = parent.get_children().map(func(item: TreeItem) -> Dictionary: + return _to_json(item)) + return item_as_dict + + +func extract_resource_path(event: GdUnitEvent) -> String: + return ProjectSettings.localize_path(event.resource_path()) + + +func collect_test_cases(item: TreeItem, tests: Array[GdUnitTestCase] = []) -> Array[GdUnitTestCase]: + for next in item.get_children(): + collect_test_cases(next, tests) + + if is_test_case(item): + var test: GdUnitTestCase = item.get_meta(META_TEST_CASE) + if not tests.has(test): + tests.append(test) + + return tests + + +func test_session_start() -> void: + _context_menu.disable_items() + reset_tree_state(_tree_root) + _report_panel.clear() + + +func test_session_stop() -> void: + _context_menu.enable_items() + abort_running() + sort_tree_items(_tree_root) + # wait until the tree redraw + await get_tree().process_frame + var failure_item := _find_first_item_by_state(_tree_root, STATE.FAILED) + select_item( failure_item if failure_item else _current_selected_item) + + +################################################################################ +# Tree signal receiver +################################################################################ +func _on_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_index: int) -> void: + if mouse_button_index == MOUSE_BUTTON_RIGHT: + _context_menu.position = get_screen_position() + mouse_position + _context_menu.popup() + + +func _on_Tree_item_selected() -> void: + _current_selected_item = _tree.get_selected() + var reports := get_item_reports(_current_selected_item) + _report_panel.show_report(reports) + tree_item_selected.emit(_current_selected_item) + + +# Opens the test suite +func _on_Tree_item_activated() -> void: + var selected_item := _tree.get_selected() + var line_number := get_item_test_line_number(selected_item) + if line_number != -1: + var script_path := ProjectSettings.localize_path(get_item_source_file(selected_item)) + var resource: Script = load(script_path) + + if selected_item.has_meta(META_GDUNIT_REPORT): + var reports := get_item_reports(selected_item) + var report_line_number := reports[0].line_number() + # if number -1 we use original stored line number of the test case + # in non debug mode the line number is not available + if report_line_number != -1: + line_number = report_line_number + + EditorInterface.get_file_system_dock().navigate_to_path(script_path) + EditorInterface.edit_script(resource, line_number) + elif selected_item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER: + # Toggle collapse if dir + selected_item.collapsed = not selected_item.collapsed + + +################################################################################ +# external signal receiver +################################################################################ + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.DISCOVER_START: + _tree_root.visible = false + _discover_hint.visible = true + init_tree() + + GdUnitEvent.DISCOVER_END: + sort_tree_items(_tree_root) + select_item(_tree_root.get_first_child()) + _discover_hint.visible = false + _tree_root.visible = true + #_dump_tree_as_json("tree_example_discovered") + + GdUnitEvent.TESTCASE_BEFORE: + update_test_case(event) + + GdUnitEvent.TESTCASE_AFTER: + update_test_case(event) + + GdUnitEvent.TESTSUITE_AFTER: + update_test_suite(event) + + GdUnitEvent.SESSION_START: + test_session_start() + + GdUnitEvent.SESSION_CLOSE: + await test_session_stop() + + +func _on_settings_changed(property :GdUnitProperty) -> void: + match property.name(): + GdUnitSettings.INSPECTOR_TREE_SORT_MODE: + sort_tree_items(_tree_root) + #_dump_tree_as_json("tree_sorted_by_%s" % GdUnitInspectorTreeConstants.SORT_MODE.keys()[property.value()]) + + GdUnitSettings.INSPECTOR_TREE_VIEW_MODE: + restructure_tree(_tree_root, GdUnitSettings.get_inspector_tree_view_mode()) diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd.uid b/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd.uid new file mode 100644 index 0000000..bab5ae2 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd.uid @@ -0,0 +1 @@ +uid://bv8m5wmgba6v0 diff --git a/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.tscn b/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.tscn new file mode 100644 index 0000000..24b6c66 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.tscn @@ -0,0 +1,133 @@ +[gd_scene format=3 uid="uid://bqfpidewtpeg0"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd" id="1"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn" id="2_o6s0p"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitReportPanel.tscn" id="3_miuuy"] + +[sub_resource type="DPITexture" id="DPITexture_njcuo"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[sub_resource type="DPITexture" id="DPITexture_o05pk"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[sub_resource type="DPITexture" id="DPITexture_njtpf"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[sub_resource type="DPITexture" id="DPITexture_eg3rq"] +_source = " +" +saturation = 2.0 +color_map = { +Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1), +Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1), +Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1) +} + +[node name="MainPanel" type="Control" unique_id=944258025] +custom_minimum_size = Vector2(120, 240) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="contextMenu" parent="." unique_id=73764904 instance=ExtResource("2_o6s0p")] +unique_name_in_owner = true +visible = false +item_0/icon = SubResource("DPITexture_njcuo") +item_1/icon = SubResource("DPITexture_o05pk") +item_2/icon = SubResource("DPITexture_njcuo") +item_4/icon = SubResource("DPITexture_njtpf") +item_5/icon = SubResource("DPITexture_eg3rq") + +[node name="split" type="VSplitContainer" parent="." unique_id=1044515559] +clip_contents = true +custom_minimum_size = Vector2(0, 160) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/autohide = 0 +split_offsets = PackedInt32Array(120) +split_offset = 120 + +[node name="Panel" type="Panel" parent="split" unique_id=290722553] +custom_minimum_size = Vector2(0, 120) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Tree" type="Tree" parent="split/Panel" unique_id=741772884] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/icon_max_width = 16 +columns = 2 +allow_reselect = true +allow_rmb_select = true +hide_root = true +select_mode = 1 + +[node name="discover_hint" type="HBoxContainer" parent="split/Panel" unique_id=555683311] +unique_name_in_owner = true +visible = false +use_parent_material = true +layout_mode = 0 +alignment = 1 + +[node name="spinner" type="Button" parent="split/Panel/discover_hint" unique_id=200472082] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(64, 64) +layout_mode = 2 +size_flags_stretch_ratio = 1.94 +disabled = true +button_mask = 0 +text = "Discover Tests" +flat = true +alignment = 2 + +[node name="report" parent="split" unique_id=1465857421 instance=ExtResource("3_miuuy")] +unique_name_in_owner = true +custom_minimum_size = Vector2(120, 40) +layout_mode = 2 + +[connection signal="item_activated" from="split/Panel/Tree" to="." method="_on_Tree_item_activated"] +[connection signal="item_mouse_selected" from="split/Panel/Tree" to="." method="_on_tree_item_mouse_selected"] +[connection signal="item_selected" from="split/Panel/Tree" to="." method="_on_Tree_item_selected"] diff --git a/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.gd b/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.gd new file mode 100644 index 0000000..889dcb1 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.gd @@ -0,0 +1,60 @@ +@tool +class_name GdUnitReportPanel +extends Panel + +@onready var report_list: Node = %report_list +@onready var message_template: RichTextLabel = %message + + +func clear() -> void: + for child in report_list.get_children(): + report_list.remove_child(child) + child.queue_free() + + +func show_report(reports: Array[GdUnitReport]) -> void: + clear() + for report in reports: + report_list.add_child(build_report(report)) + + +func build_report(report: GdUnitReport) -> RichTextLabel: + var message: RichTextLabel = message_template.duplicate() + message.push_color(GdUnitEditorColorTheme.text_color) + message.append_text(report.message()) + message.pop() + message.newline() + add_stack_trace(message, report.stack_trace()) + message.visible = true + return message + + +func add_stack_trace(message: RichTextLabel, trace: GdUnitStackTrace) -> void: + if trace == null: + return + for frame in trace.get_frames(): + message.push_indent(1) + message.push_meta(frame, RichTextLabel.META_UNDERLINE_ON_HOVER, frame._source) + message.push_color(GdUnitEditorColorTheme.text_color) + message.append_text("at ") + message.push_color(GdUnitEditorColorTheme.function_definition_color) + message.append_text(frame._function) + message.pop() + message.append_text(" in ") + message.pop() + + message.push_color(GdUnitEditorColorTheme.engine_type_color) + message.append_text(frame._source.get_file()) + message.append_text(" : ") + message.append_text(str(frame._line)) + message.pop() + message.pop() # hint + message.pop() + message.newline() + if not message.meta_clicked.is_connected(_on_meta_clicked): + message.meta_clicked.connect(_on_meta_clicked) + + +func _on_meta_clicked(meta: Variant) -> void: + var frame: GdUnitStackTraceElement = meta + GdUnitScriptEditorControls.edit_script(frame._source, frame._line) diff --git a/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.gd.uid b/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.gd.uid new file mode 100644 index 0000000..92f2fff --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.gd.uid @@ -0,0 +1 @@ +uid://cxl084808npc3 diff --git a/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.tscn b/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.tscn new file mode 100644 index 0000000..99f0f58 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/GdUnitReportPanel.tscn @@ -0,0 +1,47 @@ +[gd_scene format=3 uid="uid://dga8nhaxnyr53"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/GdUnitReportPanel.gd" id="1_w8qy0"] + +[node name="report" type="Panel" unique_id=1075065511] +clip_contents = true +custom_minimum_size = Vector2(120, 80) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_w8qy0") + +[node name="message" type="RichTextLabel" parent="." unique_id=1680360176] +unique_name_in_owner = true +auto_translate_mode = 2 +visible = false +layout_mode = 0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +focus_mode = 2 +mouse_filter = 1 +bbcode_enabled = true +fit_content = true +autowrap_mode = 0 +selection_enabled = true + +[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=385317760] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 + +[node name="report_list" type="VBoxContainer" parent="ScrollContainer" unique_id=1467163528] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 2 +theme_override_constants/separation = 2 diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd new file mode 100644 index 0000000..b5cc837 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd @@ -0,0 +1,56 @@ +@tool +class_name GdUnitInputCapture +extends Control + +signal input_completed(input_event: InputEventKey) + + +var _tween: Tween +var _input_event: InputEventKey + + +func _ready() -> void: + reset() + self_modulate = Color.WHITE + _tween = create_tween() + @warning_ignore("return_value_discarded") + _tween.set_loops() + @warning_ignore("return_value_discarded") + _tween.tween_property(%Label, "self_modulate", Color(1, 1, 1, .8), 1.0).from_current().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN_OUT) + + +func reset() -> void: + _input_event = InputEventKey.new() + + +func _input(event: InputEvent) -> void: + if not is_visible_in_tree(): + return + if event is InputEventKey and event.is_pressed() and not event.is_echo(): + var _event := event as InputEventKey + match _event.keycode: + KEY_CTRL: + _input_event.ctrl_pressed = true + KEY_SHIFT: + _input_event.shift_pressed = true + KEY_ALT: + _input_event.alt_pressed = true + KEY_META: + _input_event.meta_pressed = true + _: + _input_event.keycode = _event.keycode + _apply_input_modifiers(_event) + accept_event() + + if event is InputEventKey and not event.is_pressed(): + input_completed.emit(_input_event) + hide() + + +func _apply_input_modifiers(event: InputEvent) -> void: + if event is InputEventWithModifiers: + var _event := event as InputEventWithModifiers + _input_event.meta_pressed = _event.meta_pressed or _input_event.meta_pressed + _input_event.alt_pressed = _event.alt_pressed or _input_event.alt_pressed + _input_event.shift_pressed = _event.shift_pressed or _input_event.shift_pressed + _input_event.ctrl_pressed = _event.ctrl_pressed or _input_event.ctrl_pressed diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid new file mode 100644 index 0000000..9d9b1a0 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid @@ -0,0 +1 @@ +uid://cgkdna6ahopm7 diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn new file mode 100644 index 0000000..7e62c43 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=2 format=3 uid="uid://pmnkxrhglak5"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd" id="1_gki1u"] + +[node name="GdUnitInputMapper" type="Control"] +modulate = Color(0.929099, 0.929099, 0.929099, 0.936189) +top_level = true +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_gki1u") + +[node name="Label" type="Label" parent="."] +unique_name_in_owner = true +self_modulate = Color(0.401913, 0.401913, 0.401913, 0.461723) +top_level = true +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -60.5 +offset_top = -19.5 +offset_right = 60.5 +offset_bottom = 19.5 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_font_sizes/font_size = 26 +text = "Press keys for shortcut" diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd new file mode 100644 index 0000000..14a8bd5 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd @@ -0,0 +1,327 @@ +@tool +extends Window + +const EAXAMPLE_URL := "https://github.com/MikeSchulze/gdUnit4-examples/archive/refs/heads/master.zip" +const GdUnitTools := preload ("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdUnitUpdateClient = preload ("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +@onready var _update_client: GdUnitUpdateClient = $GdUnitUpdateClient +@onready var _version_label: RichTextLabel = %version +@onready var _btn_install: Button = %btn_install_examples +@onready var _progress_bar: ProgressBar = %ProgressBar +@onready var _progress_text: Label = %progress_lbl +@onready var _properties_template: Control = $property_template +@onready var _properties_common: Control = % "common-content" +@onready var _properties_ui: Control = % "ui-content" +@onready var _properties_shortcuts: Control = % "shortcut-content" +@onready var _properties_report: Control = % "report-content" +@onready var _input_capture: GdUnitInputCapture = %GdUnitInputCapture +@onready var _property_error: Window = % "propertyError" +@onready var _tab_container: TabContainer = %Properties +@onready var _update_tab: Control = %Update + +var _font_size: float + + +func _ready() -> void: + set_name("GdUnitSettingsDialog") + # initialize for testing + if not Engine.is_editor_hint(): + GdUnitSettings.setup() + GdUnit4Version.init_version_label(_version_label) + _font_size = GdUnitFonts.init_fonts(_version_label) + setup_properties(_properties_common, GdUnitSettings.COMMON_SETTINGS) + setup_properties(_properties_ui, GdUnitSettings.UI_SETTINGS) + setup_properties(_properties_report, GdUnitSettings.REPORT_SETTINGS) + setup_properties(_properties_shortcuts, GdUnitSettings.SHORTCUT_SETTINGS) + check_for_update() + + +func _sort_by_key(left: GdUnitProperty, right: GdUnitProperty) -> bool: + return left.name() < right.name() + + +func setup_properties(properties_parent: Control, property_category: String) -> void: + # Do remove first potential previous added properties (could be happened when the dlg is opened at twice) + for child in properties_parent.get_children(): + properties_parent.remove_child(child) + + var category_properties := GdUnitSettings.list_settings(property_category) + # sort by key + category_properties.sort_custom(_sort_by_key) + var theme_ := Theme.new() + theme_.set_constant("h_separation", "GridContainer", 12) + var last_category := "!" + var min_size_overall := 0.0 + var labels := [] + var inputs := [] + var info_labels := [] + var grid: GridContainer = null + for p in category_properties: + var min_size_ := 0.0 + var property: GdUnitProperty = p + var current_category := property.category() + if not grid or current_category != last_category: + grid = GridContainer.new() + grid.columns = 4 + grid.theme = theme_ + + var sub_category: Control = _properties_template.get_child(3).duplicate() + var category_label: Label = sub_category.get_child(0) + category_label.text = current_category.capitalize() + sub_category.custom_minimum_size.y = _font_size + 16 + properties_parent.add_child(sub_category) + properties_parent.add_child(grid) + last_category = current_category + # property name + var label: Label = _properties_template.get_child(0).duplicate() + label.text = _to_human_readable(property.name()) + labels.append(label) + grid.add_child(label) + + # property reset btn + var reset_btn: Button = _properties_template.get_child(1).duplicate() + reset_btn.icon = _get_btn_icon("Reload") + reset_btn.disabled = property.value() == property.default() + grid.add_child(reset_btn) + + # property type specific input element + var input: Node = _create_input_element(property, reset_btn) + inputs.append(input) + grid.add_child(input) + @warning_ignore("return_value_discarded") + reset_btn.pressed.connect(_on_btn_property_reset_pressed.bind(property, input, reset_btn)) + # property help text + var info: Label = _properties_template.get_child(2).duplicate() + info.text = property.help() + info_labels.append(info) + grid.add_child(info) + if min_size_overall < min_size_: + min_size_overall = min_size_ + + for controls: Array in [labels, inputs, info_labels]: + var _size: float = controls.map(func(c: Control) -> float: return c.size.x).max() + min_size_overall += _size + for control: Control in controls: + control.custom_minimum_size.x = _size + properties_parent.custom_minimum_size.x = min_size_overall + + +func _create_input_element(property: GdUnitProperty, reset_btn: Button) -> Node: + if property.is_selectable_value(): + var options := OptionButton.new() + options.alignment = HORIZONTAL_ALIGNMENT_CENTER + for value in property.value_set(): + options.add_item(value) + options.item_selected.connect(_on_option_selected.bind(property, reset_btn)) + options.select(property.int_value()) + return options + if property.type() == TYPE_BOOL: + var check_btn := CheckButton.new() + check_btn.toggled.connect(_on_property_text_changed.bind(property, reset_btn)) + check_btn.button_pressed = property.value() + return check_btn + if property.type() in [TYPE_INT, TYPE_STRING]: + var input := LineEdit.new() + input.text_changed.connect(_on_property_text_changed.bind(property, reset_btn)) + input.set_context_menu_enabled(false) + input.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER) + input.set_expand_to_text_length_enabled(true) + input.text = str(property.value()) + return input + if property.type() == TYPE_PACKED_INT32_ARRAY: + var key_input_button := Button.new() + var value:PackedInt32Array = property.value() + key_input_button.text = to_shortcut(value) + key_input_button.pressed.connect(_on_shortcut_change.bind(key_input_button, property, reset_btn)) + return key_input_button + return Control.new() + + +func to_shortcut(keys: PackedInt32Array) -> String: + var input_event := InputEventKey.new() + for key in keys: + match key: + KEY_CTRL: input_event.ctrl_pressed = true + KEY_SHIFT: input_event.shift_pressed = true + KEY_ALT: input_event.alt_pressed = true + KEY_META: input_event.meta_pressed = true + _: + input_event.keycode = key as Key + return input_event.as_text() + + +func to_keys(input_event: InputEventKey) -> PackedInt32Array: + var keys := PackedInt32Array() + if input_event.ctrl_pressed: + keys.append(KEY_CTRL) + if input_event.shift_pressed: + keys.append(KEY_SHIFT) + if input_event.alt_pressed: + keys.append(KEY_ALT) + if input_event.meta_pressed: + keys.append(KEY_META) + keys.append(input_event.keycode) + return keys + + +func _to_human_readable(value: String) -> String: + return value.split("/")[-1].capitalize() + + +func _get_btn_icon(p_name: String) -> Texture2D: + if not Engine.is_editor_hint(): + var placeholder := PlaceholderTexture2D.new() + placeholder.size = Vector2(8, 8) + return placeholder + return GdUnitUiTools.get_icon(p_name) + + +func _install_examples() -> void: + _init_progress(5) + update_progress("Downloading examples") + await get_tree().process_frame + var tmp_path := GdUnitFileAccess.create_temp_dir("download") + var zip_file := tmp_path + "/examples.zip" + var response: GdUnitUpdateClient.HttpResponse = await _update_client.request_zip_package(EAXAMPLE_URL, zip_file) + if response.status() != 200: + push_warning("Examples cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.status(), response.response()]) + update_progress("Install examples failed! Try it later again.") + await get_tree().create_timer(3).timeout + stop_progress() + return + # extract zip to tmp + update_progress("Install examples into project") + var result := GdUnitFileAccess.extract_zip(zip_file, "res://gdUnit4-examples/") + if result.is_error(): + update_progress("Install examples failed! %s" % result.error_message()) + await get_tree().create_timer(3).timeout + stop_progress() + return + update_progress("Refresh project") + await rescan() + await reimport("res://gdUnit4-examples/") + + update_progress("Examples successfully installed") + await get_tree().create_timer(3).timeout + stop_progress() + + +func rescan() -> void: + await get_tree().process_frame + var fs := EditorInterface.get_resource_filesystem() + fs.scan_sources() + while fs.is_scanning(): + await get_tree().create_timer(1).timeout + + +func reimport(path: String) -> void: + await get_tree().process_frame + var files := DirAccess.get_files_at(path) + EditorInterface.get_resource_filesystem().reimport_files(files) + for directory in DirAccess.get_directories_at(path): + reimport(directory) + + +func check_for_update() -> void: + if not GdUnitSettings.is_update_notification_enabled(): + return + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_latest_version() + if response.status() != 200: + printerr("Latest version information cannot be retrieved from GitHub!") + printerr("Error: %s" % response.response()) + return + var latest_version := _update_client.extract_latest_version(response) + if latest_version.is_greater(GdUnit4Version.current()): + var tab_index := _tab_container.get_tab_idx_from_control(_update_tab) + _tab_container.set_tab_button_icon(tab_index, GdUnitUiTools.get_icon("Notification", Color.YELLOW)) + _tab_container.set_tab_tooltip(tab_index, "An new update is available.") + + +func _on_btn_report_bug_pressed() -> void: + @warning_ignore("return_value_discarded") + OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=bug&projects=projects%2F5&template=bug_report.yml&title=GD-XXX%3A+Describe+the+issue+briefly") + + +func _on_btn_request_feature_pressed() -> void: + @warning_ignore("return_value_discarded") + OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=enhancement&projects=&template=feature_request.md&title=") + + +func _on_btn_install_examples_pressed() -> void: + _btn_install.disabled = true + await _install_examples() + _btn_install.disabled = false + + +func _on_btn_close_pressed() -> void: + hide() + + +func _on_btn_property_reset_pressed(property: GdUnitProperty, input: Node, reset_btn: Button) -> void: + if input is CheckButton: + var is_default_pressed: bool = property.default() + (input as CheckButton).button_pressed = is_default_pressed + elif input is LineEdit: + (input as LineEdit).text = str(property.default()) + # we have to update manually for text input fields because of no change event is emited + _on_property_text_changed(property.default(), property, reset_btn) + elif input is OptionButton: + (input as OptionButton).select(0) + _on_option_selected(0, property, reset_btn) + elif input is Button: + var value: PackedInt32Array = property.default() + (input as Button).text = to_shortcut(value) + _on_property_text_changed(value, property, reset_btn) + + +func _on_property_text_changed(new_value: Variant, property: GdUnitProperty, reset_btn: Button) -> void: + property.set_value(new_value) + reset_btn.disabled = property.value() == property.default() + var error: Variant = GdUnitSettings.update_property(property) + if error: + var label: Label = _property_error.get_child(0) as Label + label.set_text(str(error)) + var control := gui_get_focus_owner() + _property_error.show() + if control != null: + _property_error.position = control.global_position + Vector2(self.position) + Vector2(40, 40) + + +func _on_option_selected(index: int, property: GdUnitProperty, reset_btn: Button) -> void: + property.set_value(index) + reset_btn.disabled = property.value() == property.default() + GdUnitSettings.update_property(property) + + +func _on_shortcut_change(input_button: Button, property: GdUnitProperty, reset_btn: Button) -> void: + _input_capture.set_custom_minimum_size(_properties_shortcuts.get_size()) + _input_capture.visible = true + _input_capture.show() + _properties_shortcuts.visible = false + set_process_input(false) + _input_capture.reset() + var input_event: InputEventKey = await _input_capture.input_completed + input_button.text = input_event.as_text() + _on_property_text_changed(to_keys(input_event), property, reset_btn) + _properties_shortcuts.visible = true + set_process_input(true) + + +func _init_progress(max_value: int) -> void: + _progress_bar.visible = true + _progress_bar.max_value = max_value + _progress_bar.value = 0 + + +func _progress() -> void: + _progress_bar.value += 1 + + +func stop_progress() -> void: + _progress_bar.visible = false + + +func update_progress(message: String) -> void: + _progress_text.text = message + _progress_bar.value += 1 diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid new file mode 100644 index 0000000..1e5369e --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid @@ -0,0 +1 @@ +uid://bv2oiqcuvtbl2 diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn new file mode 100644 index 0000000..4f74431 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn @@ -0,0 +1,349 @@ +[gd_scene load_steps=9 format=3] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"] +[ext_resource type="Texture2D" path="res://addons/gdUnit4/src/ui/settings/logo.png" id="3_isfyl"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.tscn" id="4"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn" id="4_nf72w"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn" id="5_n1jtv"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbbq5"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.172549, 0.113725, 0.141176, 1) +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.87451, 0.0705882, 0.160784, 1) +border_blend = true +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +shadow_color = Color(0, 0, 0, 0.756863) +shadow_size = 10 +shadow_offset = Vector2(10, 10) + +[node name="GdUnitSettingsDialog" type="Window"] +disable_3d = true +gui_embed_subwindows = true +title = "GdUnit4 Settings" +initial_position = 1 +size = Vector2i(1400, 600) +visible = false +wrap_controls = true +exclusive = true +extend_to_title = true +script = ExtResource("2") + +[node name="property_template" type="Control" parent="."] +visible = false +layout_mode = 3 +anchors_preset = 0 +offset_left = 4.0 +offset_top = 4.0 +offset_right = 4.0 +offset_bottom = 4.0 +size_flags_horizontal = 0 + +[node name="Label" type="Label" parent="property_template"] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_top = -11.5 +offset_right = 1.0 +offset_bottom = 11.5 +grow_vertical = 2 + +[node name="btn_reset" type="Button" parent="property_template"] +layout_mode = 0 +offset_right = 12.0 +offset_bottom = 40.0 +tooltip_text = "Reset to default value" +clip_text = true + +[node name="info" type="Label" parent="property_template"] +clip_contents = true +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_top = -11.5 +offset_right = 316.0 +offset_bottom = 11.5 +grow_vertical = 2 +size_flags_horizontal = 3 +max_lines_visible = 1 + +[node name="sub_category" type="Panel" parent="property_template"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_right = -220.0 +grow_horizontal = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="property_template/sub_category"] +layout_mode = 1 +anchors_preset = 4 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = 4.0 +offset_top = -11.5 +offset_right = 5.0 +offset_bottom = 11.5 +grow_vertical = 2 +theme_override_colors/font_color = Color(0.439216, 0.45098, 1, 1) + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("8_2ggr0") + +[node name="Panel" type="Panel" parent="."] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="PanelContainer" type="PanelContainer" parent="Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="v" type="VBoxContainer" parent="Panel/PanelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="Panel/PanelContainer/v"] +use_parent_material = true +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 + +[node name="GridContainer" type="HBoxContainer" parent="Panel/PanelContainer/v/MarginContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="PanelContainer" type="MarginContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Panel" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="CenterContainer" type="CenterContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="logo" type="TextureRect" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer"] +custom_minimum_size = Vector2(120, 120) +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +texture = ExtResource("3_isfyl") +expand_mode = 1 +stretch_mode = 5 + +[node name="CenterContainer2" type="MarginContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel"] +use_parent_material = true +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="version" type="RichTextLabel" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer2"] +unique_name_in_owner = true +auto_translate_mode = 2 +use_parent_material = true +clip_contents = false +layout_mode = 2 +size_flags_horizontal = 3 +localize_numeral_system = false +bbcode_enabled = true +scroll_active = false +meta_underlined = false + +[node name="VBoxContainer" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 2 + +[node name="btn_report_bug" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to create a bug report" +text = "Report Bug" + +[node name="btn_request_feature" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to create a feature request" +text = "Request Feature" + +[node name="btn_install_examples" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to install the advanced test examples" +disabled = true +text = "Install Examples" + +[node name="Properties" type="TabContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +current_tab = 0 + +[node name="Common" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="common-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Common"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(1026, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Hooks" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4_nf72w")] +visible = false +layout_mode = 2 + +[node name="UI" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 +metadata/_tab_index = 2 + +[node name="ui-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/UI"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(741, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Shortcuts" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 +metadata/_tab_index = 3 + +[node name="shortcut-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Shortcuts"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(683, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Report" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 +metadata/_tab_index = 4 + +[node name="report-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Report"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(667, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Templates" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4")] +visible = false +layout_mode = 2 +metadata/_tab_index = 5 + +[node name="Update" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_n1jtv")] +unique_name_in_owner = true +visible = false +layout_mode = 2 +metadata/_tab_index = 6 + +[node name="GdUnitInputCapture" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_xu3j8")] +unique_name_in_owner = true +visible = false +modulate = Color(1.54884e-09, 1.54884e-09, 1.54884e-09, 0.1) +z_index = 1 +z_as_relative = false +layout_mode = 2 +size_flags_horizontal = 1 +size_flags_vertical = 1 + +[node name="propertyError" type="PopupPanel" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] +unique_name_in_owner = true +initial_position = 1 +size = Vector2i(400, 100) +theme_override_styles/panel = SubResource("StyleBoxFlat_hbbq5") + +[node name="Label" type="Label" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/propertyError"] +offset_left = 10.0 +offset_top = 4.0 +offset_right = 390.0 +offset_bottom = 96.0 +theme_override_colors/font_color = Color(0.858824, 0, 0.109804, 1) +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="MarginContainer2" type="MarginContainer" parent="Panel/PanelContainer/v"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/PanelContainer/v/MarginContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 +alignment = 2 + +[node name="ProgressBar" type="ProgressBar" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="progress_lbl" type="Label" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer/ProgressBar"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +clip_text = true + +[node name="btn_close" type="Button" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Close" + +[connection signal="close_requested" from="." to="." method="_on_btn_close_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_report_bug" to="." method="_on_btn_report_bug_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_request_feature" to="." method="_on_btn_request_feature_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_install_examples" to="." method="_on_btn_install_examples_pressed"] +[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer2/HBoxContainer/btn_close" to="." method="_on_btn_close_pressed"] diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd new file mode 100644 index 0000000..dacb48e --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd @@ -0,0 +1,208 @@ +@tool +extends ScrollContainer + + +@onready var _hooks_list: VBoxContainer = %hook_list +@onready var _hook_row_template: Panel = %row_template +@onready var _hook_description: RichTextLabel = %hook_description +@onready var _btn_move_up: Button = %hook_actions/btn_move_up +@onready var _btn_move_down: Button = %hook_actions/btn_move_down +@onready var _btn_delete: Button = %hook_actions/btn_delete_hook +@onready var _select_hook_dlg: FileDialog = %select_hook_dlg +@onready var _error_msg_popup: AcceptDialog = %error_msg_popup + + +var _selected_row: Control = null +var selected_style := StyleBoxFlat.new() + + +func _ready() -> void: + selected_style.set_border_color(Color.DODGER_BLUE) + selected_style.set_border_width_all(2) + selected_style.set_corner_radius_all(4) + selected_style.set_draw_center(false) + + for hook: GdUnitTestSessionHook in GdUnitTestSessionHookService.instance().enigne_hooks: + _create_hook_row(hook) + + _update_focus_relations() + + if _hooks_list.get_child_count() > 0: + _select_row(_hooks_list.get_child(0)) + + +func _update_focus_relations() -> void: + var row_count := _hooks_list.get_child_count() + for hook_index in row_count: + var current_hook: Control = _hooks_list.get_child(hook_index) + var previous_hook: Control = current_hook if hook_index == 0 else _hooks_list.get_child(hook_index - 1) + var next_hook: Control = current_hook if hook_index >= row_count - 1 else _hooks_list.get_child(hook_index + 1) + current_hook.focus_neighbor_top = previous_hook.get_path() + current_hook.set_focus_previous(previous_hook.get_path()) + current_hook.focus_neighbor_bottom = next_hook.get_path() + current_hook.set_focus_next(next_hook.get_path()) + + +func _create_hook_row(hook: GdUnitTestSessionHook) -> Control: + var is_system := _is_system_hook(hook) + var panel: Panel = _hook_row_template.duplicate() + + panel.visible = true + panel.set_meta("hook", hook) + panel.tooltip_text = "System hook - (Read-only)" if is_system else "User hook" + panel.focus_entered.connect(_select_row.bind(panel)) + panel.gui_input.connect(func(event: InputEvent) -> void: + @warning_ignore("unsafe_property_access") + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + _select_row(panel) + panel.accept_event() + ) + + # Left color indicator border + var indicator: ColorRect = panel.find_child("Indicator", true, false) + indicator.color = Color(1.0, 0.76, 0.03, 1) if is_system else Color(0.26, 0.54, 0.89, 1) + + # Hook name + var name_label: Label = panel.find_child("HookName", true, false) + name_label.text = hook.name + + # System badge + if is_system: + var badge: Control = panel.find_child("SystemBadge", true, false) + badge.visible = true + + # Enable/disable toggle + var check: CheckButton = panel.find_child("Enabled", true, false) + check.button_pressed = GdUnitTestSessionHookService.is_enabled(hook) + check.toggled.connect(func(enabled: bool) -> void: + GdUnitTestSessionHookService.instance().enable_hook(hook, enabled) + ) + + _hooks_list.add_child(panel) + return panel + + +func _select_row(row: Node) -> void: + if _selected_row == row: + return + + if _selected_row != null and is_instance_valid(_selected_row): + _selected_row.remove_theme_stylebox_override("panel") + + _selected_row = row + + if _selected_row != null: + _selected_row.grab_focus() + + _selected_row.add_theme_stylebox_override("panel", selected_style) + + _update_hook_buttons() + _update_hook_description() + + +func _update_hook_description() -> void: + if _selected_row == null: + _hook_description.text = "[i]Select a hook to view its description[/i]" + return + _hook_description.text = _get_hook(_selected_row).description + + +func _update_hook_buttons() -> void: + if _selected_row == null: + _btn_move_up.disabled = true + _btn_move_down.disabled = true + _btn_delete.disabled = true + return + + var hook := _get_hook(_selected_row) + if _is_system_hook(hook): + _btn_move_up.disabled = true + _btn_move_down.disabled = true + _btn_delete.disabled = true + return + + var idx := _selected_row.get_index() + var count := _hooks_list.get_child_count() + var prev: Control = _hooks_list.get_child(idx - 1) if idx > 0 else null + var next: Control = _hooks_list.get_child(idx + 1) if idx + 1 < count else null + + _btn_move_up.disabled = prev == null or _is_system_hook(_get_hook(prev)) + _btn_move_down.disabled = next == null + _btn_delete.disabled = false + + +static func _get_hook(row: Control) -> GdUnitTestSessionHook: + return row.get_meta("hook") + + +static func _is_system_hook(hook: GdUnitTestSessionHook) -> bool: + if hook == null: + return false + return hook.get_meta("SYSTEM_HOOK", false) + + +func _on_btn_add_hook_pressed() -> void: + _select_hook_dlg.show() + + +func _on_select_hook_dlg_file_selected(path: String) -> void: + _select_hook_dlg.set_current_path(path) + _on_select_hook_dlg_confirmed() + + +func _on_select_hook_dlg_confirmed() -> void: + _select_hook_dlg.hide() + var result := GdUnitTestSessionHookService.instance().load_hook(_select_hook_dlg.get_current_path()) + if result.is_error(): + _error_msg_popup.dialog_text = result.error_message() + _error_msg_popup.show() + return + + var hook: GdUnitTestSessionHook = result.value() + result = GdUnitTestSessionHookService.instance().register(hook) + if result.is_error(): + _error_msg_popup.dialog_text = result.error_message() + _error_msg_popup.show() + return + + var row := _create_hook_row(hook) + _update_focus_relations() + _select_row(row) + + +func _on_btn_delete_hook_pressed() -> void: + if _selected_row == null: + return + GdUnitTestSessionHookService.instance().unregister(_get_hook(_selected_row)) + + _hooks_list.remove_child(_selected_row) + _selected_row.queue_free() + + _update_focus_relations() + _select_row(_hooks_list.find_next_valid_focus()) + + +func _on_btn_move_up_pressed() -> void: + if _selected_row == null: + return + var idx := _selected_row.get_index() - 1 + if idx <= 0: + return + var prev: Control = _hooks_list.get_child(idx) + _hooks_list.move_child(_selected_row, idx) + GdUnitTestSessionHookService.instance().move_before(_get_hook(_selected_row), _get_hook(prev)) + _update_focus_relations() + _update_hook_buttons() + + +func _on_btn_move_down_pressed() -> void: + if _selected_row == null: + return + var idx := _selected_row.get_index() + 1 + if idx >= _hooks_list.get_child_count(): + return + var next: Control = _hooks_list.get_child(idx) + _hooks_list.move_child(_selected_row, idx) + GdUnitTestSessionHookService.instance().move_after(_get_hook(_selected_row), _get_hook(next)) + _update_focus_relations() + _update_hook_buttons() diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid new file mode 100644 index 0000000..94bda32 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid @@ -0,0 +1 @@ +uid://c5bule3kyypht diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn new file mode 100644 index 0000000..3d241c2 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn @@ -0,0 +1,227 @@ +[gd_scene format=3 uid="uid://41l7a46fol5m"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd" id="1_8yffn"] + +[sub_resource type="Image" id="Image_h5sr5"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 240, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 228, 228, 228, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_77fm0"] +image = SubResource("Image_h5sr5") + +[sub_resource type="Image" id="Image_77fm0"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 239, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 227, 227, 227, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_rewru"] +image = SubResource("Image_77fm0") + +[sub_resource type="Image" id="Image_kppp6"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_manhx"] +image = SubResource("Image_kppp6") + +[sub_resource type="Image" id="Image_rewru"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 227, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 73, 224, 224, 224, 226, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_4h4u1"] +image = SubResource("Image_rewru") + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h5sr5"] +content_margin_left = 4.0 +content_margin_top = 2.0 +content_margin_right = 4.0 +content_margin_bottom = 2.0 +bg_color = Color(1, 0.85490197, 0.011764706, 1) +border_width_left = 2 +border_width_top = 4 +border_width_right = 2 +border_width_bottom = 4 +border_color = Color(0.8, 0.8, 0.8, 0) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[node name="Hooks" type="ScrollContainer" unique_id=659565185] +custom_minimum_size = Vector2(400, 300) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_8yffn") +metadata/_tab_index = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1746177026] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="hooks_content" type="VBoxContainer" parent="HBoxContainer" unique_id=1756623882] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Panel" type="Panel" parent="HBoxContainer/hooks_content" unique_id=1467652907] +modulate = Color(0.827451, 0.827451, 0.827451, 1) +layout_mode = 2 +size_flags_vertical = 3 + +[node name="hook_scroll" type="ScrollContainer" parent="HBoxContainer/hooks_content/Panel" unique_id=1296435931] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 + +[node name="hook_list" type="VBoxContainer" parent="HBoxContainer/hooks_content/Panel/hook_scroll" unique_id=1329270012] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="hook_description" type="RichTextLabel" parent="HBoxContainer/hooks_content" unique_id=1554657128] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 120) +layout_mode = 2 +size_flags_vertical = 2 +bbcode_enabled = true +text = "" +scroll_active = false + +[node name="hook_actions" type="VBoxContainer" parent="HBoxContainer" unique_id=1441205618] +unique_name_in_owner = true +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +size_flags_horizontal = 8 +theme_override_constants/separation = 5 + +[node name="btn_move_up" type="Button" parent="HBoxContainer/hook_actions" unique_id=1909528536] +layout_mode = 2 +tooltip_text = "Move hook up in priority" +disabled = true +icon = SubResource("ImageTexture_77fm0") +icon_alignment = 1 + +[node name="btn_move_down" type="Button" parent="HBoxContainer/hook_actions" unique_id=1385113071] +layout_mode = 2 +tooltip_text = "Move hook down in priority" +disabled = true +icon = SubResource("ImageTexture_rewru") +icon_alignment = 1 + +[node name="btn_add_hook" type="Button" parent="HBoxContainer/hook_actions" unique_id=338115845] +layout_mode = 2 +tooltip_text = "Add new hook" +icon = SubResource("ImageTexture_manhx") +icon_alignment = 1 + +[node name="btn_delete_hook" type="Button" parent="HBoxContainer/hook_actions" unique_id=642037387] +layout_mode = 2 +tooltip_text = "Delete selected hook" +disabled = true +icon = SubResource("ImageTexture_4h4u1") +icon_alignment = 1 + +[node name="select_hook_dlg" type="FileDialog" parent="." unique_id=2043681690] +unique_name_in_owner = true +disable_3d = true +title = "Open a File" +initial_position = 3 +current_screen = 0 +ok_button_text = "Open" +file_mode = 0 +filters = PackedStringArray("*.gd") + +[node name="error_msg_popup" type="AcceptDialog" parent="." unique_id=1430197292] +unique_name_in_owner = true +initial_position = 3 +current_screen = 0 + +[node name="row_template" type="Panel" parent="." unique_id=2068077019] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(0, 26) +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="row_template" unique_id=421161803] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Spacer1" type="MarginContainer" parent="row_template/HBoxContainer" unique_id=1388548361] +clip_contents = true +custom_minimum_size = Vector2(12, 0) +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 3 +theme_override_constants/margin_top = 3 +theme_override_constants/margin_bottom = 3 + +[node name="Indicator" type="ColorRect" parent="row_template/HBoxContainer/Spacer1" unique_id=695255812] +custom_minimum_size = Vector2(4, 0) +layout_mode = 2 +color = Color(1, 1, 0, 1) + +[node name="HookName" type="Label" parent="row_template/HBoxContainer" unique_id=167022167] +layout_mode = 2 +size_flags_horizontal = 2 +text = "" + +[node name="SystemBadge" type="PanelContainer" parent="row_template/HBoxContainer" unique_id=1220764160] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_styles/panel = SubResource("StyleBoxFlat_h5sr5") + +[node name="Label" type="Label" parent="row_template/HBoxContainer/SystemBadge" unique_id=1972968522] +layout_mode = 2 +theme_override_colors/font_color = Color(0.008202022, 0.008202025, 0.008202025, 1) +theme_override_font_sizes/font_size = 10 +text = "SYSTEM" + +[node name="Enabled" type="CheckButton" parent="row_template/HBoxContainer" unique_id=365063095] +layout_mode = 2 +size_flags_vertical = 4 +tooltip_text = "Enable/Disable the Hook" +focus_mode = 0 + +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_up" to="." method="_on_btn_move_up_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_down" to="." method="_on_btn_move_down_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_add_hook" to="." method="_on_btn_add_hook_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_delete_hook" to="." method="_on_btn_delete_hook_pressed"] +[connection signal="confirmed" from="select_hook_dlg" to="." method="_on_select_hook_dlg_confirmed"] +[connection signal="file_selected" from="select_hook_dlg" to="." method="_on_select_hook_dlg_file_selected"] diff --git a/addons/gdUnit4/src/ui/settings/logo.png b/addons/gdUnit4/src/ui/settings/logo.png new file mode 100644 index 0000000..12de79f Binary files /dev/null and b/addons/gdUnit4/src/ui/settings/logo.png differ diff --git a/addons/gdUnit4/src/ui/settings/logo.png.import b/addons/gdUnit4/src/ui/settings/logo.png.import new file mode 100644 index 0000000..f3beedb --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/logo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b048g57qf7jys" +path="res://.godot/imported/logo.png-deda0e4ba02a0b9e4e4a830029a5817f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/settings/logo.png" +dest_files=["res://.godot/imported/logo.png-deda0e4ba02a0b9e4e4a830029a5817f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd b/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd new file mode 100644 index 0000000..be5c352 --- /dev/null +++ b/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd @@ -0,0 +1,129 @@ +@tool +extends MarginContainer + +@onready var _template_editor :CodeEdit = $VBoxContainer/EdiorLayout/Editor +@onready var _tags_editor :CodeEdit = $Tags/MarginContainer/TextEdit +@onready var _title_bar :Panel = $VBoxContainer/sub_category +@onready var _save_button :Button = $VBoxContainer/Panel/HBoxContainer/Save +@onready var _selected_type :OptionButton = $VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/SelectType +@onready var _show_tags :PopupPanel = $Tags + + +var gd_key_words :PackedStringArray = ["extends", "class_name", "const", "var", "onready", "func", "void", "pass"] +var gdunit_key_words :PackedStringArray = ["GdUnitTestSuite", "before", "after", "before_test", "after_test"] +var _selected_template :int + + +func _ready() -> void: + setup_editor_colors() + setup_fonts() + setup_supported_types() + load_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + setup_tags_help() + + +func _notification(what :int) -> void: + if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: + setup_fonts() + + +func setup_editor_colors() -> void: + if not Engine.is_editor_hint(): + return + + var background_color := get_editor_color("text_editor/theme/highlighting/background_color", Color(0.1155, 0.132, 0.1595, 1)) + var text_color := get_editor_color("text_editor/theme/highlighting/text_color", Color(0.8025, 0.81, 0.8225, 1)) + var selection_color := get_editor_color("text_editor/theme/highlighting/selection_color", Color(0.44, 0.73, 0.98, 0.4)) + + for e :CodeEdit in [_template_editor, _tags_editor]: + var editor :CodeEdit = e + editor.add_theme_color_override("background_color", background_color) + editor.add_theme_color_override("font_color", text_color) + editor.add_theme_color_override("font_readonly_color", text_color) + editor.add_theme_color_override("font_selected_color", selection_color) + setup_highlighter(editor) + + +func setup_highlighter(editor :CodeEdit) -> void: + var highlighter := CodeHighlighter.new() + editor.set_syntax_highlighter(highlighter) + var number_color := get_editor_color("text_editor/theme/highlighting/number_color", Color(0.63, 1, 0.88, 1)) + var symbol_color := get_editor_color("text_editor/theme/highlighting/symbol_color", Color(0.67, 0.79, 1, 1)) + var function_color := get_editor_color("text_editor/theme/highlighting/function_color", Color(0.34, 0.7, 1, 1)) + var member_variable_color := get_editor_color("text_editor/theme/highlighting/member_variable_color", Color(0.736, 0.88, 1, 1)) + var comment_color := get_editor_color("text_editor/theme/highlighting/comment_color", Color(0.8025, 0.81, 0.8225, 0.5)) + var keyword_color := get_editor_color("text_editor/theme/highlighting/keyword_color", Color(1, 0.44, 0.52, 1)) + var base_type_color := get_editor_color("text_editor/theme/highlighting/base_type_color", Color(0.26, 1, 0.76, 1)) + var annotation_color := get_editor_color("text_editor/theme/highlighting/gdscript/annotation_color", Color(1, 0.7, 0.45, 1)) + + highlighter.clear_color_regions() + highlighter.clear_keyword_colors() + highlighter.add_color_region("#", "", comment_color, true) + highlighter.add_color_region("${", "}", Color.YELLOW) + highlighter.add_color_region("'", "'", Color.YELLOW) + highlighter.add_color_region("\"", "\"", Color.YELLOW) + highlighter.number_color = number_color + highlighter.symbol_color = symbol_color + highlighter.function_color = function_color + highlighter.member_variable_color = member_variable_color + highlighter.add_keyword_color("@", annotation_color) + highlighter.add_keyword_color("warning_ignore", annotation_color) + for word in gd_key_words: + highlighter.add_keyword_color(word, keyword_color) + for word in gdunit_key_words: + highlighter.add_keyword_color(word, base_type_color) + + +## Using this function to avoid null references to colors on inital Godot installations. +## For more details show https://github.com/MikeSchulze/gdUnit4/issues/533 +func get_editor_color(property_name: String, default: Color) -> Color: + var settings := EditorInterface.get_editor_settings() + return settings.get_setting(property_name) if settings.has_setting(property_name) else default + + +func setup_fonts() -> void: + if _template_editor: + @warning_ignore("return_value_discarded") + GdUnitFonts.init_fonts(_template_editor) + var font_size := GdUnitFonts.init_fonts(_tags_editor) + _title_bar.size.y = font_size + 16 + _title_bar.custom_minimum_size.y = font_size + 16 + + +func setup_supported_types() -> void: + _selected_type.clear() + _selected_type.add_item("GD - GDScript", GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + _selected_type.add_item("C# - CSharpScript", GdUnitTestSuiteTemplate.TEMPLATE_ID_CS) + + +func setup_tags_help() -> void: + _tags_editor.set_text(GdUnitTestSuiteTemplate.load_tags(_selected_template)) + + +func load_template(template_id :int) -> void: + _selected_template = template_id + _template_editor.set_text(GdUnitTestSuiteTemplate.load_template(template_id)) + + +func _on_Restore_pressed() -> void: + _template_editor.set_text(GdUnitTestSuiteTemplate.default_template(_selected_template)) + GdUnitTestSuiteTemplate.reset_to_default(_selected_template) + _save_button.disabled = true + + +func _on_Save_pressed() -> void: + GdUnitTestSuiteTemplate.save_template(_selected_template, _template_editor.get_text()) + _save_button.disabled = true + + +func _on_Tags_pressed() -> void: + _show_tags.popup_centered_ratio(.5) + + +func _on_Editor_text_changed() -> void: + _save_button.disabled = false + + +func _on_SelectType_item_selected(index :int) -> void: + load_template(_selected_type.get_item_id(index)) + setup_tags_help() diff --git a/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd.uid b/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd.uid new file mode 100644 index 0000000..a2bd797 --- /dev/null +++ b/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd.uid @@ -0,0 +1 @@ +uid://v1dfjjyi3aoe diff --git a/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.tscn b/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.tscn new file mode 100644 index 0000000..c90eba1 --- /dev/null +++ b/addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.tscn @@ -0,0 +1,127 @@ +[gd_scene format=3 uid="uid://dte0m2endcgtu"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd" id="1"] + +[node name="TestSuiteTemplate" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="sub_category" type="Panel" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VBoxContainer/sub_category"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 4.0 +offset_right = 4.0 +offset_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +text = "Test Suite Template +" + +[node name="EdiorLayout" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Editor" type="CodeEdit" parent="VBoxContainer/EdiorLayout"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/EdiorLayout/Editor"] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -31.0 +grow_horizontal = 2 +grow_vertical = 0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer"] +layout_mode = 2 +size_flags_vertical = 8 +alignment = 2 + +[node name="Tags" type="Button" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"] +layout_mode = 2 +tooltip_text = "Shows supported tags." +text = "Supported Tags" + +[node name="SelectType" type="OptionButton" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"] +layout_mode = 2 +tooltip_text = "Select the script type specific template." +item_count = 2 +selected = 0 +popup/item_0/text = "GD - GDScript" +popup/item_0/id = 1000 +popup/item_1/text = "C# - CSharpScript" +popup/item_1/id = 2000 + +[node name="Panel" type="MarginContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Panel"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 2 + +[node name="Restore" type="Button" parent="VBoxContainer/Panel/HBoxContainer"] +layout_mode = 2 +text = "Restore" + +[node name="Save" type="Button" parent="VBoxContainer/Panel/HBoxContainer"] +layout_mode = 2 +disabled = true +text = "Save" + +[node name="Tags" type="PopupPanel" parent="."] +size = Vector2i(300, 100) +unresizable = false +content_scale_aspect = 4 + +[node name="MarginContainer" type="MarginContainer" parent="Tags"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 4.0 +offset_top = 4.0 +offset_right = -856.0 +offset_bottom = -552.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TextEdit" type="CodeEdit" parent="Tags/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +editable = false +context_menu_enabled = false +shortcut_keys_enabled = false +virtual_keyboard_enabled = false + +[connection signal="text_changed" from="VBoxContainer/EdiorLayout/Editor" to="." method="_on_Editor_text_changed"] +[connection signal="pressed" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/Tags" to="." method="_on_Tags_pressed"] +[connection signal="item_selected" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/SelectType" to="." method="_on_SelectType_item_selected"] +[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Restore" to="." method="_on_Restore_pressed"] +[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Save" to="." method="_on_Save_pressed"] diff --git a/addons/gdUnit4/src/ui/utils/GdUnitEditorColorTheme.gd b/addons/gdUnit4/src/ui/utils/GdUnitEditorColorTheme.gd new file mode 100644 index 0000000..fd2419a --- /dev/null +++ b/addons/gdUnit4/src/ui/utils/GdUnitEditorColorTheme.gd @@ -0,0 +1,51 @@ +## Provides editor theme colors sourced from [EditorSettings]. +## Add this node to the scene tree so it auto-refreshes colors when the +## editor theme changes ([constant EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED]). +@tool +class_name GdUnitEditorColorTheme +extends Node + + +static var text_color := Color.WEB_GRAY +static var folder_color := Color.LIGHT_SKY_BLUE +static var function_definition_color := Color.ANTIQUE_WHITE +static var engine_type_color := Color.ANTIQUE_WHITE +static var value_color := Color.DODGER_BLUE + +# test state colors +static var state_initial := Color.LIGHT_GRAY +static var state_success := Color.WEB_GREEN +static var state_warning := Color.DARK_GOLDENROD +static var state_flaky := Color.GREEN_YELLOW +static var state_failure := Color.ORANGE_RED +static var state_error := Color.DARK_RED +static var state_skipped := Color.WEB_GRAY +static var state_orphan := Color.DARK_GOLDENROD + + +func _ready() -> void: + setup() + + +func _notification(what: int) -> void: + if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: + setup() + + +static func setup() -> void: + if Engine.is_editor_hint(): + var settings := EditorInterface.get_editor_settings() + text_color = settings.get_setting("text_editor/theme/highlighting/text_color") + folder_color = settings.get_setting("text_editor/theme/highlighting/member_variable_color") + function_definition_color = settings.get_setting("text_editor/theme/highlighting/gdscript/function_definition_color") + engine_type_color = settings.get_setting("text_editor/theme/highlighting/engine_type_color") + value_color = settings.get_setting("text_editor/theme/highlighting/function_color") + # init test state colors + state_initial = text_color + state_success = settings.get_setting("editors/animation/onion_layers_future_color") + state_warning = settings.get_setting("text_editor/theme/highlighting/comment_markers/warning_color") + state_flaky = settings.get_setting("text_editor/theme/highlighting/gdscript/node_reference_color") + state_failure = settings.get_setting("text_editor/theme/highlighting/comment_markers/critical_color") + state_error = settings.get_setting("editors/2d/smart_snapping_line_color") + state_skipped = settings.get_setting("text_editor/theme/highlighting/member_variable_color") + state_orphan = settings.get_setting("text_editor/theme/highlighting/string_placeholder_color") diff --git a/addons/gdUnit4/src/ui/utils/GdUnitEditorColorTheme.gd.uid b/addons/gdUnit4/src/ui/utils/GdUnitEditorColorTheme.gd.uid new file mode 100644 index 0000000..ec70781 --- /dev/null +++ b/addons/gdUnit4/src/ui/utils/GdUnitEditorColorTheme.gd.uid @@ -0,0 +1 @@ +uid://mu4uxgvjgol7 diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd b/addons/gdUnit4/src/update/GdMarkDownReader.gd new file mode 100644 index 0000000..dab19e4 --- /dev/null +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd @@ -0,0 +1,405 @@ +@tool +extends RefCounted + +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +const FONT_H1 := 22 +const FONT_H2 := 20 +const FONT_H3 := 18 +const FONT_H4 := 16 +const FONT_H5 := 14 +const FONT_H6 := 12 + +const HORIZONTAL_RULE := "[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img]" +const HEADER_RULE := "[font_size=%d]$1[/font_size]" +const HEADER_CENTERED_RULE := "[font_size=%d][center]$1[/center][/font_size]" + +const image_download_folder := "res://addons/gdUnit4/tmp-update/" + +const exclude_font_size := "\b(?!(?:(font_size))\b)" + +var md_replace_patterns := [ + # comments + [regex("(?m)^\\n?\\s*\\s*\\n?"), ""], + + # horizontal rules + [regex("(?m)^[ ]{0,3}---$"), HORIZONTAL_RULE], + [regex("(?m)^[ ]{0,3}___$"), HORIZONTAL_RULE], + [regex("(?m)^[ ]{0,3}\\*\\*\\*$"), HORIZONTAL_RULE], + + # headers + [regex("(?m)^###### (.*)"), HEADER_RULE % FONT_H6], + [regex("(?m)^##### (.*)"), HEADER_RULE % FONT_H5], + [regex("(?m)^#### (.*)"), HEADER_RULE % FONT_H4], + [regex("(?m)^### (.*)"), HEADER_RULE % FONT_H3], + [regex("(?m)^## (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], + [regex("(?m)^# (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("(?m)^(.+)=={2,}$"), HEADER_RULE % FONT_H1], + [regex("(?m)^(.+)--{2,}$"), HEADER_RULE % FONT_H2], + # html headers + [regex("

((.*?\\R?)+)<\\/h1>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("((.*?\\R?)+)<\\/h1>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("

((.*?\\R?)+)<\\/h2>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], + [regex("((.*?\\R?)+)<\\/h2>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("

((.*?\\R?)+)<\\/h3>"), HEADER_RULE % FONT_H3], + [regex("((.*?\\R?)+)<\\/h3>"), HEADER_CENTERED_RULE % FONT_H3], + [regex("

((.*?\\R?)+)<\\/h4>"), HEADER_RULE % FONT_H4], + [regex("((.*?\\R?)+)<\\/h4>"), HEADER_CENTERED_RULE % FONT_H4], + [regex("
((.*?\\R?)+)<\\/h5>"), HEADER_RULE % FONT_H5], + [regex("((.*?\\R?)+)<\\/h5>"), HEADER_CENTERED_RULE % FONT_H5], + [regex("
((.*?\\R?)+)<\\/h6>"), HEADER_RULE % FONT_H6], + [regex("((.*?\\R?)+)<\\/h6>"), HEADER_CENTERED_RULE % FONT_H6], + + # asterics + #[regex("(\\*)"), "xxx$1xxx"], + + # extract/compile image references + [regex("!\\[(.*?)\\]\\[(.*?)\\]"), process_image_references], + # extract images with path and optional tool tip + [regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)"), process_image], + + # links + [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)\\)"), "[url={\"url\":\"$3\"}]$2[/url]"], + # links with tool tip + [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)( \"(.+)\")?\\)"), "[url={\"url\":\"$3\", \"tool_tip\":\"$5\"}]$2[/url]"], + # links to github, as shorted link + [regex("(https://github.*/?/(\\S+))"), '[url={"url":"$1", "tool_tip":"$1"}]#$2[/url]'], + + # embeded text + [regex("(?m)^[ ]{0,3}>(.*?)$"), "[img=50x14]res://addons/gdUnit4/src/update/assets/embedded.png[/img][i]$1[/i]"], + + # italic + bold font + [regex("[_]{3}(.*?)[_]{3}"), "[i][b]$1[/b][/i]"], + [regex("[\\*]{3}(.*?)[\\*]{3}"), "[i][b]$1[/b][/i]"], + # bold font + [regex("(.*?)<\\/b>"), "[b]$1[/b]"], + [regex("[_]{2}(.*?)[_]{2}"), "[b]$1[/b]"], + [regex("[\\*]{2}(.*?)[\\*]{2}"), "[b]$1[/b]"], + # italic font + [regex("(.*?)<\\/i>"), "[i]$1[/i]"], + [regex(exclude_font_size+"_(.*?)_"), "[i]$1[/i]"], + [regex("\\*(.*?)\\*"), "[i]$1[/i]"], + + # strikethrough font + [regex("(.*?)"), "[s]$1[/s]"], + [regex("~~(.*?)~~"), "[s]$1[/s]"], + [regex("~(.*?)~"), "[s]$1[/s]"], + + # handling lists + # using an image for dots + [regex("(?m)^[ ]{0,1}[*\\-+] (.*)$"), list_replace(0)], + [regex("(?m)^[ ]{2,3}[*\\-+] (.*)$"), list_replace(1)], + [regex("(?m)^[ ]{4,5}[*\\-+] (.*)$"), list_replace(2)], + [regex("(?m)^[ ]{6,7}[*\\-+] (.*)$"), list_replace(3)], + [regex("(?m)^[ ]{8,9}[*\\-+] (.*)$"), list_replace(4)], + + # code + [regex("``([\\s\\S]*?)``"), code_block("$1")], + [regex("`([\\s\\S]*?)`{1,2}"), code_block("$1")], +] + +var code_block_patterns := [ + # code blocks, code blocks looks not like code blocks in richtext + [regex("```(javascript|python|shell|gdscript|gd)([\\s\\S]*?\n)```"), code_block("$2", true)], +] + +var _img_replace_regex := RegEx.new() +var _image_urls := PackedStringArray() +var _on_table_tag := false +var _client: GdUnitUpdateClient + + +static func regex(pattern: String) -> RegEx: + var regex_ := RegEx.new() + var err := regex_.compile(pattern) + if err != OK: + push_error("error '%s' checked pattern '%s'" % [err, pattern]) + return null + return regex_ + + +func _init() -> void: + @warning_ignore("return_value_discarded") + _img_replace_regex.compile("\\[img\\]((.*?))\\[/img\\]") + + +func set_http_client(client: GdUnitUpdateClient) -> void: + _client = client + + +@warning_ignore("return_value_discarded") +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + # finally remove_at the downloaded images + for image in _image_urls: + DirAccess.remove_absolute(image) + DirAccess.remove_absolute(image + ".import") + + +func list_replace(indent: int) -> String: + var replace_pattern := "[img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img]" if indent %2 else "[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img]" + replace_pattern += " $1" + + for index in indent: + replace_pattern = replace_pattern.insert(0, " ") + return replace_pattern + + +func code_block(replace: String, border: bool = false) -> String: + if border: + return """ + [img=1400x14]res://addons/gdUnit4/src/update/assets/border_top.png[/img] + [indent][color=GRAY][font_size=16]%s[/font_size][/color][/indent] + [img=1400x14]res://addons/gdUnit4/src/update/assets/border_bottom.png[/img] + """.dedent() % replace + return "[code][bgcolor=DARK_SLATE_GRAY][color=GRAY][font_size=16]%s[/font_size][/color][/bgcolor][/code]" % replace + + +func convert_text(input: String) -> String: + input = process_tables(input) + + for pattern: Array in md_replace_patterns: + var regex_: RegEx = pattern[0] + var bb_replace: Variant = pattern[1] + if bb_replace is Callable: + @warning_ignore("unsafe_method_access") + input = await bb_replace.call(regex_, input) + else: + @warning_ignore("unsafe_cast") + input = regex_.sub(input, bb_replace as String, true) + return input + + +func convert_code_block(input: String) -> String: + for pattern: Array in code_block_patterns: + var regex_: RegEx = pattern[0] + var bb_replace: Variant = pattern[1] + if bb_replace is Callable: + @warning_ignore("unsafe_method_access") + input = await bb_replace.call(regex_, input) + else: + @warning_ignore("unsafe_cast") + input = regex_.sub(input, bb_replace as String, true) + return input + + +func to_bbcode(input: String) -> String: + var re := regex("(?m)```[\\s\\S]*?```") + var current_pos := 0 + var as_bbcode := "" + + # we split by code blocks to handle this blocks customized + for result in re.search_all(input): + # Add text before code block + if result.get_start() > current_pos: + as_bbcode += await convert_text(input.substr(current_pos, result.get_start() - current_pos)) + # Add code block + as_bbcode += await convert_code_block(result.get_string()) + current_pos = result.get_end() + + # Add remaining text after last code block + if current_pos < input.length(): + as_bbcode += await convert_text(input.substr(current_pos)) + return as_bbcode + + +func process_tables(input: String) -> String: + var bbcode := PackedStringArray() + var lines: Array[String] = Array(input.split("\n") as Array, TYPE_STRING, "", null) + while not lines.is_empty(): + if is_table(lines[0]): + bbcode.append_array(parse_table(lines)) + continue + @warning_ignore("return_value_discarded", "unsafe_cast") + bbcode.append(lines.pop_front() as String) + return "\n".join(bbcode) + + +class GdUnitMDReaderTable: + var _columns: int + var _rows: Array[Row] = [] + + class Row: + var _cells := PackedStringArray() + + + func _init(cells: PackedStringArray, columns: int) -> void: + _cells = cells + for i in range(_cells.size(), columns): + @warning_ignore("return_value_discarded") + _cells.append("") + + + func to_bbcode(cell_sizes: PackedInt32Array, bold: bool) -> String: + var cells := PackedStringArray() + for cell_index in _cells.size(): + var cell: String = _cells[cell_index] + if cell.strip_edges() == "--": + cell = create_line(cell_sizes[cell_index]) + if bold: + cell = "[b]%s[/b]" % cell + @warning_ignore("return_value_discarded") + cells.append("[cell]%s[/cell]" % cell) + return "|".join(cells) + + + func create_line(length: int) -> String: + var line := "" + for i in length: + line += "-" + return line + + + func _init(columns: int) -> void: + _columns = columns + + + func parse_row(line :String) -> bool: + # is line containing cells? + if line.find("|") == -1: + return false + _rows.append(Row.new(line.split("|"), _columns)) + return true + + + func calculate_max_cell_sizes() -> PackedInt32Array: + var cells_size := PackedInt32Array() + for column in _columns: + @warning_ignore("return_value_discarded") + cells_size.append(0) + + for row_index in _rows.size(): + var row: Row = _rows[row_index] + for cell_index in row._cells.size(): + var cell_size: int = cells_size[cell_index] + var size := row._cells[cell_index].length() + if size > cell_size: + cells_size[cell_index] = size + return cells_size + + + @warning_ignore("return_value_discarded") + func to_bbcode() -> PackedStringArray: + var cell_sizes := calculate_max_cell_sizes() + var bb_code := PackedStringArray() + + bb_code.append("[table=%d]" % _columns) + for row_index in _rows.size(): + bb_code.append(_rows[row_index].to_bbcode(cell_sizes, row_index==0)) + bb_code.append("[/table]\n") + return bb_code + + +func parse_table(lines: Array) -> PackedStringArray: + var line: String = lines[0] + var table := GdUnitMDReaderTable.new(line.count("|") + 1) + while not lines.is_empty(): + line = lines.pop_front() + if not table.parse_row(line): + break + return table.to_bbcode() + + +func is_table(line: String) -> bool: + return line.find("|") != -1 + + +func open_table(line: String) -> String: + _on_table_tag = true + return "[table=%d]" % (line.count("|") + 1) + + +func close_table() -> String: + _on_table_tag = false + return "[/table]" + + +func extract_cells(line: String, bold := false) -> String: + var cells := "" + for cell in line.split("|"): + if bold: + cell = "[b]%s[/b]" % cell + cells += "[cell]%s[/cell]" % cell + return cells + + +func process_image_references(p_regex: RegEx, p_input: String) -> String: + #return p_input + + # exists references? + var matches := p_regex.search_all(p_input) + if matches.is_empty(): + return p_input + # collect image references and remove_at it + var references := Dictionary() + var link_regex := regex("\\[(\\S+)\\]:(\\S+)([ ]\"(.*)\")?") + # create copy of original source to replace checked it + var input := p_input.replace("\r", "") + var extracted_references := p_input.replace("\r", "") + for reg_match in link_regex.search_all(input): + var line := reg_match.get_string(0) + "\n" + var ref := reg_match.get_string(1) + #var topl_tip = reg_match.get_string(4) + # collect reference and url + references[ref] = reg_match.get_string(2) + extracted_references = extracted_references.replace(line, "") + + # replace image references by collected url's + for reference_key: String in references.keys(): + var regex_key := regex("\\](\\[%s\\])" % reference_key) + for reg_match in regex_key.search_all(extracted_references): + var ref: String = reg_match.get_string(0) + var image_url: String = "](%s)" % references.get(reference_key) + extracted_references = extracted_references.replace(ref, image_url) + return extracted_references + + +@warning_ignore("return_value_discarded") +func process_image(p_regex: RegEx, p_input: String) -> String: + #return p_input + var to_replace := PackedStringArray() + var tool_tips := PackedStringArray() + # find all matches + var matches := p_regex.search_all(p_input) + if matches.is_empty(): + return p_input + for reg_match in matches: + # grap the parts to replace and store temporay because a direct replace will distort the offsets + to_replace.append(p_input.substr(reg_match.get_start(0), reg_match.get_end(0))) + # grap optional tool tips + tool_tips.append(reg_match.get_string(5)) + # finally replace all findings + for replace in to_replace: + var re := p_regex.sub(replace, "[img]$2[/img]") + p_input = p_input.replace(replace, re) + return await _process_external_image_resources(p_input) + + +func _process_external_image_resources(input: String) -> String: + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(image_download_folder) + # scan all img for external resources and download it + for value in _img_replace_regex.search_all(input): + if value.get_group_count() >= 1: + var image_url: String = value.get_string(1) + # if not a local resource we need to download it + if image_url.begins_with("http"): + if OS.is_stdout_verbose(): + prints("download image:", image_url) + var response := await _client.request_image(image_url) + if response.status() == 200: + var image := Image.new() + var error := image.load_png_from_buffer(response.get_body()) + if error != OK: + prints("Error creating image from response", error) + # replace characters where format characters + var new_url := image_download_folder + image_url.get_file().replace("_", "-") + if new_url.get_extension() != 'png': + new_url = new_url + '.png' + var err := image.save_png(new_url) + if err: + push_error("Can't save image to '%s'. Error: %s" % [new_url, error_string(err)]) + @warning_ignore("return_value_discarded") + _image_urls.append(new_url) + input = input.replace(image_url, new_url) + return input diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid new file mode 100644 index 0000000..4a78321 --- /dev/null +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid @@ -0,0 +1 @@ +uid://dy2f5iqjnsav2 diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd b/addons/gdUnit4/src/update/GdUnitPatch.gd new file mode 100644 index 0000000..daa20f7 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd @@ -0,0 +1,20 @@ +class_name GdUnitPatch +extends RefCounted + +const PATCH_VERSION = "patch_version" + +var _version :GdUnit4Version + + +func _init(version_ :GdUnit4Version) -> void: + _version = version_ + + +func version() -> GdUnit4Version: + return _version + + +# this function needs to be implement +func execute() -> bool: + push_error("The function 'execute()' is not implemented at %s" % self) + return false diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd.uid b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid new file mode 100644 index 0000000..7fb167b --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid @@ -0,0 +1 @@ +uid://dw8jltfrqpsvy diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd b/addons/gdUnit4/src/update/GdUnitPatcher.gd new file mode 100644 index 0000000..73d25c9 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd @@ -0,0 +1,75 @@ +class_name GdUnitPatcher +extends RefCounted + + +const _base_dir := "res://addons/gdUnit4/src/update/patches/" + +var _patches := Dictionary() + + +func scan(current :GdUnit4Version) -> void: + _scan(_base_dir, current) + + +func _scan(scan_path :String, current :GdUnit4Version) -> void: + _patches = Dictionary() + var patch_paths := _collect_patch_versions(scan_path, current) + for path in patch_paths: + prints("scan for patches checked '%s'" % path) + _patches[path] = _scan_patches(path) + + +func patch_count() -> int: + var count := 0 + for key :String in _patches.keys(): + @warning_ignore("unsafe_method_access") + count += _patches[key].size() + return count + + +func execute() -> void: + for key :String in _patches.keys(): + for path :String in _patches[key]: + var patch :GdUnitPatch = (load(key + "/" + path) as GDScript).new() + if patch: + prints("execute patch", patch.version(), patch.get_script().resource_path) + if not patch.execute(): + prints("error checked execution patch %s" % key + "/" + path) + + +func _collect_patch_versions(scan_path :String, current :GdUnit4Version) -> PackedStringArray: + if not DirAccess.dir_exists_absolute(scan_path): + return PackedStringArray() + var patches := Array() + var dir := DirAccess.open(scan_path) + if dir != null: + @warning_ignore("return_value_discarded") + dir.list_dir_begin() # TODO GODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + var version := GdUnit4Version.parse(next) + if version.is_greater(current): + patches.append(scan_path + next) + patches.sort() + return PackedStringArray(patches) + + +func _scan_patches(path :String) -> PackedStringArray: + var patches := Array() + var dir := DirAccess.open(path) + if dir != null: + @warning_ignore("return_value_discarded") + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var next := "." + while next != "": + next = dir.get_next() + # step over directory links and .uid files + if next.is_empty() or next == "." or next == ".." or next.ends_with(".uid"): + continue + patches.append(next) + # make sorted from lowest to high version + patches.sort() + return PackedStringArray(patches) diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid new file mode 100644 index 0000000..88703b4 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid @@ -0,0 +1 @@ +uid://c37u42n646ym8 diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd new file mode 100644 index 0000000..97b7fdd --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -0,0 +1,305 @@ +@tool +extends Container + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const GDUNIT_TEMP := "user://tmp" + +@onready var _progress_content: RichTextLabel = %message +@onready var _progress_bar: TextureProgressBar = %progress +@onready var _cancel_btn: Button = %cancel +@onready var _update_btn: Button = %update +@onready var _spinner_img := GdUnitUiTools.get_spinner() + + +var _debug_mode := false +var _update_client :GdUnitUpdateClient +var _download_url :String + + +func _ready() -> void: + init_progress(6) + + +func _process(_delta :float) -> void: + if _progress_content != null and _progress_content.is_visible_in_tree(): + _progress_content.queue_redraw() + + +func init_progress(max_value: int) -> void: + _cancel_btn.disabled = false + _update_btn.disabled = false + _progress_bar.max_value = max_value + _progress_bar.value = 1 + message_h4("Press [Update] to start.", Color.GREEN, false) + + +func setup(update_client: GdUnitUpdateClient, download_url: String) -> void: + _update_client = update_client + _download_url = download_url + + +func update_progress(message: String, color := Color.GREEN) -> void: + message_h4(message, color) + _progress_bar.value += 1 + if _debug_mode: + await get_tree().create_timer(3).timeout + await get_tree().create_timer(.2).timeout + + +func _colored(message: String, color: Color) -> String: + return "[color=#%s]%s[/color]" % [color.to_html(), message] + + +func message_h4(message: String, color: Color, show_spinner := true) -> void: + _progress_content.clear() + if show_spinner: + _progress_content.add_image(_spinner_img) + _progress_content.append_text(" [font_size=16]%s[/font_size]" % _colored(message, color)) + if _debug_mode: + prints(message) + + +@warning_ignore("return_value_discarded") +func run_update() -> void: + _cancel_btn.disabled = true + _update_btn.disabled = true + + await update_progress("Downloading the update.") + await download_release() + await update_progress("Extracting") + var zip_file := temp_dir() + "/update.zip" + var tmp_path := create_temp_dir("update") + var result :Variant = extract_zip(zip_file, tmp_path) + if result == null: + await update_progress("Update failed! .. Rollback.", Color.INDIAN_RED) + await get_tree().create_timer(3).timeout + _cancel_btn.disabled = false + _update_btn.disabled = false + init_progress(5) + hide() + return + + await update_progress("Uninstall GdUnit4.") + disable_gdUnit() + if not _debug_mode: + GdUnitFileAccess.delete_directory("res://addons/gdUnit4/") + # give editor time to react on deleted files + await get_tree().create_timer(1).timeout + + await update_progress("Install new GdUnit4 version.") + if _debug_mode: + copy_directory(tmp_path, "res://debug") + else: + copy_directory(tmp_path, "res://") + + await update_progress("Patch invalid UID's") + await patch_uids() + + await rebuild_project() + + await update_progress("New GdUnit version successfully installed, Restarting Godot please wait.") + await get_tree().create_timer(3).timeout + enable_gdUnit() + hide() + GdUnitFileAccess.delete_directory("res://addons/.gdunit_update") + restart_godot() + + +func patch_uids(path := "res://addons/gdUnit4/src/") -> void: + var to_reimport: PackedStringArray + for file in DirAccess.get_files_at(path): + var file_path := path.path_join(file) + var ext := file.get_extension() + + if ext == "tscn" or ext == "scn" or ext == "tres" or ext == "res": + message_h4("Patch GdUnit4 scene: '%s'" % file, Color.WEB_GREEN) + remove_uids_from_file(file_path) + elif FileAccess.file_exists(file_path + ".import"): + to_reimport.append(file_path) + + if not to_reimport.is_empty(): + message_h4("Reimport resources '%s'" % ", ".join(to_reimport), Color.WEB_GREEN) + if Engine.is_editor_hint(): + EditorInterface.get_resource_filesystem().reimport_files(to_reimport) + + for dir in DirAccess.get_directories_at(path): + if not dir.begins_with("."): + patch_uids(path.path_join(dir)) + await get_tree().process_frame + + +func remove_uids_from_file(file_path: String) -> bool: + var file := FileAccess.open(file_path, FileAccess.READ) + if file == null: + print("Failed to open file: ", file_path) + return false + + var original_content := file.get_as_text() + file.close() + + # Remove UIDs using regex + var regex := RegEx.new() + regex.compile("(\\[ext_resource[^\\]]*?)\\s+uid=\"uid://[^\"]*\"") + + var modified_content := regex.sub(original_content, "$1", true) + + # Check if any changes were made + if original_content != modified_content: + prints("Patched invalid uid's out in '%s'" % file_path) + # Write the modified content back + file = FileAccess.open(file_path, FileAccess.WRITE) + if file == null: + print("Failed to write to file: ", file_path) + return false + + file.store_string(modified_content) + file.close() + return true + + return false + + +func restart_godot() -> void: + prints("Force restart Godot") + EditorInterface.restart_editor(true) + + +@warning_ignore("return_value_discarded") +func enable_gdUnit() -> void: + var enabled_plugins := PackedStringArray() + if ProjectSettings.has_setting("editor_plugins/enabled"): + enabled_plugins = ProjectSettings.get_setting("editor_plugins/enabled") + if not enabled_plugins.has("res://addons/gdUnit4/plugin.cfg"): + enabled_plugins.append("res://addons/gdUnit4/plugin.cfg") + ProjectSettings.set_setting("editor_plugins/enabled", enabled_plugins) + ProjectSettings.save() + + +func disable_gdUnit() -> void: + EditorInterface.set_plugin_enabled("gdUnit4", false) + + +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 + + +func create_temp_dir(folder_name :String) -> String: + var new_folder := temp_dir() + "/" + folder_name + GdUnitFileAccess.delete_directory(new_folder) + if not DirAccess.dir_exists_absolute(new_folder): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(new_folder) + return new_folder + + +func copy_directory(from_dir: String, to_dir: String) -> bool: + if not DirAccess.dir_exists_absolute(from_dir): + printerr("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: + printerr("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(): + @warning_ignore("return_value_discarded") + copy_directory(source + "/", dest) + continue + var err := source_dir.copy(source, dest) + if err != OK: + printerr("Error checked copy file '%s' to '%s'" % [source, dest]) + return false + return true + else: + printerr("Directory not found: " + from_dir) + return false + + +func extract_zip(zip_package: String, dest_path: String) -> Variant: + var zip: ZIPReader = ZIPReader.new() + var err := zip.open(zip_package) + if err != OK: + printerr("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) + return null + 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 dest_path + + +func download_release() -> void: + var zip_file := GdUnitFileAccess.temp_dir() + "/update.zip" + var response :GdUnitUpdateClient.HttpResponse + if _debug_mode: + response = GdUnitUpdateClient.HttpResponse.new(200, PackedByteArray()) + zip_file = "res://update.zip" + return + + response = await _update_client.request_zip_package(_download_url, zip_file) + if response.status() != 200: + push_warning("Update information cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.status(), response.response()]) + message_h4("Download the update failed! Try it later again.", Color.INDIAN_RED) + await get_tree().create_timer(3).timeout + + +func rebuild_project() -> void: + # Check if this is a Godot .NET runtime instance + if not ClassDB.class_exists("CSharpScript"): + return + + update_progress("Rebuild the project ...") + await get_tree().process_frame + + var output := [] + var exit_code := OS.execute("dotnet", ["build"], output) + if exit_code == -1: + message_h4("Rebuild the project failed, check your project dependencies.", Color.INDIAN_RED) + await get_tree().create_timer(3).timeout + return + + for out: String in output: + print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges()) + await get_tree().process_frame + + +func _on_confirmed() -> void: + await run_update() + + +func _on_cancel_pressed() -> void: + hide() + + +func _on_update_pressed() -> void: + await run_update() diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid new file mode 100644 index 0000000..61d2c4d --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid @@ -0,0 +1 @@ +uid://ftpro37j1mro diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.tscn b/addons/gdUnit4/src/update/GdUnitUpdate.tscn new file mode 100644 index 0000000..20d60b7 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.tscn @@ -0,0 +1,100 @@ +[gd_scene load_steps=6 format=3 uid="uid://2eahgaw88y6q"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdate.gd" id="1"] + +[sub_resource type="Gradient" id="Gradient_wilsr"] +colors = PackedColorArray(0.151276, 0.151276, 0.151276, 1, 1, 1, 1, 1) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_45cww"] +gradient = SubResource("Gradient_wilsr") +fill_to = Vector2(0.75641, 0) + +[sub_resource type="Gradient" id="Gradient_i0qp8"] +colors = PackedColorArray(1, 1, 1, 1, 0.20871, 0.20871, 0.20871, 1) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_wilsr"] +gradient = SubResource("Gradient_i0qp8") +fill_from = Vector2(0.794872, 0) +fill_to = Vector2(0, 0) + +[node name="GdUnitUpdate" type="MarginContainer"] +clip_contents = true +custom_minimum_size = Vector2(0, 80) +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 80.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_right = 10 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="message" type="RichTextLabel" parent="VBoxContainer/Panel"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +bbcode_enabled = true +text = "aaaaa" +fit_content = true +scroll_active = false +shortcut_keys_enabled = false + +[node name="Panel2" type="Panel" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="progress" type="TextureProgressBar" parent="VBoxContainer/Panel2"] +unique_name_in_owner = true +auto_translate_mode = 2 +clip_contents = true +custom_minimum_size = Vector2(0, 20) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +localize_numeral_system = false +min_value = 1.0 +max_value = 5.0 +value = 1.0 +rounded = true +allow_greater = true +nine_patch_stretch = true +texture_under = SubResource("GradientTexture2D_45cww") +texture_progress = SubResource("GradientTexture2D_wilsr") +tint_under = Color(0.0235294, 0.145098, 0.168627, 1) +tint_progress = Color(0.288912, 0.233442, 0.533772, 1) + +[node name="PanelContainer" type="MarginContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 +alignment = 2 + +[node name="update" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Update" + +[node name="cancel" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Cancel" + +[connection signal="pressed" from="VBoxContainer/PanelContainer/HBoxContainer/update" to="." method="_on_update_pressed"] +[connection signal="pressed" from="VBoxContainer/PanelContainer/HBoxContainer/cancel" to="." method="_on_cancel_pressed"] diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd new file mode 100644 index 0000000..b4f54b7 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd @@ -0,0 +1,98 @@ +@tool +extends Node + +signal request_completed(response: HttpResponse) + +class HttpResponse: + var _http_status: int + var _body: PackedByteArray + + + func _init(http_status: int, body: PackedByteArray) -> void: + _http_status = http_status + _body = body + + + func status() -> int: + return _http_status + + + func response() -> Variant: + if _http_status != 200: + return _body.get_string_from_utf8() + + var test_json_conv := JSON.new() + @warning_ignore("return_value_discarded") + var error := test_json_conv.parse(_body.get_string_from_utf8()) + if error != OK: + return "HttpResponse: %s Error: %s" % [error_string(error), _body.get_string_from_utf8()] + return test_json_conv.get_data() + + func get_body() -> PackedByteArray: + return _body + + +var _http_request := HTTPRequest.new() + + +func _ready() -> void: + add_child(_http_request) + @warning_ignore("return_value_discarded") + _http_request.request_completed.connect(_on_request_completed) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + if is_instance_valid(_http_request): + _http_request.queue_free() + + +#func list_tags() -> void: +# _http_request.connect("request_completed",Callable(self,"_response_request_tags")) +# var error = _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags") +# if error != OK: +# push_error("An error occurred in the HTTP request.") + + +func request_latest_version() -> HttpResponse: + var error := _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags") + if error != OK: + var message := "Request latest version failed, %s" % error_string(error) + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_releases() -> HttpResponse: + var error := _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/releases") + if error != OK: + var message := "request_releases failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_image(url: String) -> HttpResponse: + var error := _http_request.request(url) + if error != OK: + var message := "request_image failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_zip_package(url: String, file: String) -> HttpResponse: + _http_request.set_download_file(file) + var error := _http_request.request(url) + if error != OK: + var message := "request_zip_package failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func extract_latest_version(response: HttpResponse) -> GdUnit4Version: + var body: Array = response.response() + return GdUnit4Version.parse(str(body[0]["name"])) + + +func _on_request_completed(_result: int, response_http_status: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + if _http_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: + _http_request.set_download_file("") + request_completed.emit(HttpResponse.new(response_http_status, body)) diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid new file mode 100644 index 0000000..8d550b0 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid @@ -0,0 +1 @@ +uid://exh5yeioidrs diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd new file mode 100644 index 0000000..aba5e18 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd @@ -0,0 +1,206 @@ +@tool +extends MarginContainer + +#signal request_completed(response) + +const GdMarkDownReader = preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd") +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const GdUnitUpdateProgress = preload("res://addons/gdUnit4/src/update/GdUnitUpdate.gd") + +@onready var _md_reader: GdMarkDownReader = GdMarkDownReader.new() +@onready var _update_client: GdUnitUpdateClient = $GdUnitUpdateClient +@onready var _header: Label = $Panel/GridContainer/PanelContainer/header +@onready var _update_button: Button = $Panel/GridContainer/Panel/HBoxContainer/update +@onready var _content: RichTextLabel = $Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content +@onready var _update_progress :GdUnitUpdateProgress = %update_banner + +var _debug_mode := false +var _patcher := GdUnitPatcher.new() +var _current_version := GdUnit4Version.current() + + +func _ready() -> void: + _update_button.set_disabled(false) + _md_reader.set_http_client(_update_client) + @warning_ignore("return_value_discarded") + #GdUnitFonts.init_fonts(_content) + _update_progress.set_visible(false) + _update_progress.hidden.connect(func() -> void: + _update_button.set_disabled(false) + ) + + +func request_releases() -> bool: + if _debug_mode: + _update_progress._debug_mode = _debug_mode + _header.text = "A new version 'v4.4.4' is available" + _update_button.set_disabled(false) + return true + + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_latest_version() + if response.status() != 200: + _header.text = "Update information cannot be retrieved from GitHub!" + message_h4("\n\nError: %s" % response.response(), Color.INDIAN_RED) + return false + var latest_version := _update_client.extract_latest_version(response) + # if same version exit here no update need + if latest_version.is_greater(_current_version): + _patcher.scan(_current_version) + _header.text = "A new version '%s' is available" % latest_version + var download_zip_url := extract_zip_url(response) + _update_progress.setup(_update_client, download_zip_url) + _update_button.set_disabled(false) + return true + else: + _header.text = "No update is available." + _update_button.set_disabled(true) + return false + + +func _colored(message_: String, color: Color) -> String: + return "[color=#%s]%s[/color]" % [color.to_html(), message_] + + +func message_h4(message_: String, color: Color, clear := true) -> void: + if clear: + _content.clear() + _content.append_text("[font_size=16]%s[/font_size]" % _colored(message_, color)) + + +func message(message_: String, color: Color) -> void: + _content.clear() + _content.append_text(_colored(message_, color)) + + +func _process(_delta: float) -> void: + if _content != null and _content.is_visible_in_tree(): + _content.queue_redraw() + + +func show_update() -> void: + if not GdUnitSettings.is_update_notification_enabled(): + _header.text = "No update is available." + message_h4("The search for updates is deactivated.", Color.CORNFLOWER_BLUE) + _update_button.set_disabled(true) + return + + if not await request_releases(): + return + _update_button.set_disabled(true) + + prints("Scan for GdUnit4 Update ...") + message_h4("\n\n\nRequest release infos ... ", Color.SNOW) + _content.add_image(GdUnitUiTools.get_spinner(), 32, 32) + + var content: String + if _debug_mode: + await get_tree().create_timer(.2).timeout + var template := FileAccess.open("res://addons/gdUnit4/test/update/resources/http_response_releases.txt", FileAccess.READ).get_as_text() + content = await _md_reader.to_bbcode(template) + else: + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_releases() + if response.status() == 200: + content = await extract_releases(response, _current_version) + else: + message_h4("\n\n\nError checked request available releases!", Color.INDIAN_RED) + return + + # finally force rescan to import images as textures + if Engine.is_editor_hint(): + await rescan() + message(content, Color.CADET_BLUE) + _update_button.set_disabled(false) + + + +func extract_zip_url(response: GdUnitUpdateClient.HttpResponse) -> String: + var body :Array = response.response() + return body[0]["zipball_url"] + + +func extract_releases(response: GdUnitUpdateClient.HttpResponse, current_version: GdUnit4Version) -> String: + await get_tree().process_frame + var result := "" + for release :Dictionary in response.response(): + var release_version := str(release["tag_name"]) + if GdUnit4Version.parse(release_version).equals(current_version): + break + var release_description := _colored("

GdUnit Release %s

" % release_version, Color.CORNFLOWER_BLUE) + release_description += "\n" + release_description += release["body"] + release_description += "\n\n" + result += await _md_reader.to_bbcode(release_description) + return result + + +func rescan() -> void: + if Engine.is_editor_hint(): + if OS.is_stdout_verbose(): + prints(".. reimport release resources") + var fs := EditorInterface.get_resource_filesystem() + fs.scan() + while fs.is_scanning(): + if OS.is_stdout_verbose(): + progressBar(fs.get_scanning_progress() * 100 as int) + await get_tree().process_frame + await get_tree().process_frame + await get_tree().create_timer(1).timeout + + +func progressBar(p_progress: int) -> void: + if p_progress < 0: + p_progress = 0 + if p_progress > 100: + p_progress = 100 + printraw("scan [%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "#").rpad(50, "-"), p_progress]) + + +@warning_ignore("return_value_discarded") +func _on_update_pressed() -> void: + _update_button.set_disabled(true) + # close all opend scripts before start the update + if not _debug_mode: + GdUnitScriptEditorControls.close_open_editor_scripts() + # copy update source to a temp because the update is deleting the whole gdUnit folder + DirAccess.make_dir_absolute("res://addons/.gdunit_update") + DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", "res://addons/.gdunit_update/GdUnitUpdate.tscn") + DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd") + var source := FileAccess.open("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", FileAccess.READ) + var content := source.get_as_text().replace("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd") + var dest := FileAccess.open("res://addons/.gdunit_update/GdUnitUpdate.tscn", FileAccess.WRITE) + dest.store_string(content) + _update_progress.set_visible(true) + + +func _on_show_next_toggled(enabled: bool) -> void: + GdUnitSettings.set_update_notification(enabled) + + +func _on_cancel_pressed() -> void: + hide() + + +func _on_content_meta_clicked(meta: String) -> void: + var properties: Dictionary = str_to_var(meta) + if properties.has("url"): + @warning_ignore("return_value_discarded") + OS.shell_open(str(properties.get("url"))) + + +func _on_content_meta_hover_started(meta: String) -> void: + var properties: Dictionary = str_to_var(meta) + if properties.has("tool_tip"): + _content.set_tooltip_text(str(properties.get("tool_tip"))) + + +@warning_ignore("unused_parameter") +func _on_content_meta_hover_ended(meta: String) -> void: + _content.set_tooltip_text("") + + +func _on_visibility_changed() -> void: + if not is_visible_in_tree(): + return + if _update_progress != null: + _update_progress.set_visible(false) + await show_update() diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid new file mode 100644 index 0000000..bb93965 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid @@ -0,0 +1 @@ +uid://bouqsvkqt7yc3 diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn new file mode 100644 index 0000000..46119f1 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn @@ -0,0 +1,97 @@ +[gd_scene load_steps=4 format=3 uid="uid://0xyeci1tqebj"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.gd" id="1_112wo"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="2_18asx"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdate.tscn" id="3_x87h6"] + +[node name="Control" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_112wo") + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("2_18asx") + +[node name="Panel" type="Panel" parent="."] +layout_mode = 2 + +[node name="GridContainer" type="VBoxContainer" parent="Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 1 + +[node name="PanelContainer" type="MarginContainer" parent="Panel/GridContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="header" type="Label" parent="Panel/GridContainer/PanelContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 9 + +[node name="PanelContainer2" type="PanelContainer" parent="Panel/GridContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="Panel/GridContainer/PanelContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="Panel/GridContainer/PanelContainer2/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="content" type="RichTextLabel" parent="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +bbcode_enabled = true + +[node name="update_banner" parent="Panel/GridContainer" instance=ExtResource("3_x87h6")] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 1 +size_flags_vertical = 8 + +[node name="Panel" type="MarginContainer" parent="Panel/GridContainer"] +layout_mode = 2 +size_flags_vertical = 8 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/GridContainer/Panel"] +use_parent_material = true +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="update" type="Button" parent="Panel/GridContainer/Panel/HBoxContainer"] +custom_minimum_size = Vector2(100, 40) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +text = "Update" + +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] +[connection signal="meta_clicked" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_clicked"] +[connection signal="meta_hover_ended" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_ended"] +[connection signal="meta_hover_started" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_started"] +[connection signal="pressed" from="Panel/GridContainer/Panel/HBoxContainer/update" to="." method="_on_update_pressed"] diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png b/addons/gdUnit4/src/update/assets/border_bottom.png new file mode 100644 index 0000000..aa16bb7 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/border_bottom.png differ diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png.import b/addons/gdUnit4/src/update/assets/border_bottom.png.import new file mode 100644 index 0000000..72e90c6 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_bottom.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ncaq7v2hnq1v" +path="res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/border_bottom.png" +dest_files=["res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/border_top.png b/addons/gdUnit4/src/update/assets/border_top.png new file mode 100644 index 0000000..b1b1039 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/border_top.png differ diff --git a/addons/gdUnit4/src/update/assets/border_top.png.import b/addons/gdUnit4/src/update/assets/border_top.png.import new file mode 100644 index 0000000..9ef3341 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_top.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3k1sw1effu06" +path="res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/border_top.png" +dest_files=["res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/dot1.png b/addons/gdUnit4/src/update/assets/dot1.png new file mode 100644 index 0000000..d5ea77b Binary files /dev/null and b/addons/gdUnit4/src/update/assets/dot1.png differ diff --git a/addons/gdUnit4/src/update/assets/dot1.png.import b/addons/gdUnit4/src/update/assets/dot1.png.import new file mode 100644 index 0000000..47cc752 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c86vvbvef67q6" +path="res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/dot1.png" +dest_files=["res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/dot2.png b/addons/gdUnit4/src/update/assets/dot2.png new file mode 100644 index 0000000..4a74498 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/dot2.png differ diff --git a/addons/gdUnit4/src/update/assets/dot2.png.import b/addons/gdUnit4/src/update/assets/dot2.png.import new file mode 100644 index 0000000..cf83796 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cfvmwbo583ygi" +path="res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/dot2.png" +dest_files=["res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/embedded.png b/addons/gdUnit4/src/update/assets/embedded.png new file mode 100644 index 0000000..15cb9ab Binary files /dev/null and b/addons/gdUnit4/src/update/assets/embedded.png differ diff --git a/addons/gdUnit4/src/update/assets/embedded.png.import b/addons/gdUnit4/src/update/assets/embedded.png.import new file mode 100644 index 0000000..ad4cbc1 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/embedded.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://df8nb5n6c51u4" +path="res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/embedded.png" +dest_files=["res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png b/addons/gdUnit4/src/update/assets/horizontal-line2.png new file mode 100644 index 0000000..66aa098 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/horizontal-line2.png differ diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png.import b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import new file mode 100644 index 0000000..296baf2 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://by6k6wis1jxmq" +path="res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/horizontal-line2.png" +dest_files=["res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/test/CLAUDE.md b/addons/gdUnit4/test/CLAUDE.md new file mode 100644 index 0000000..9183191 --- /dev/null +++ b/addons/gdUnit4/test/CLAUDE.md @@ -0,0 +1,230 @@ +# Testing Requirements + +Every code change must be accompanied by new or updated tests. Tests live under `addons/gdUnit4/test/` and mirror +the `src/` structure (e.g. `src/core/Foo.gd` โ†’ `test/core/FooTest.gd`). + +## GdUnit4 Fluent Syntax + +All GDScript tests must use the GdUnit4 fluent assertion API. Do **not** use plain `assert()`. + +**Test suite skeleton:** + +```gdscript +class_name MyFeatureTest +extends GdUnitTestSuite + + +const __source = "res://addons/gdUnit4/src/path/to/MyFeature.gd" + + +# optional lifecycle hooks +func before_test() -> void: + pass + +func after_test() -> void: + pass +``` + +**Test section grouping โ€” use `#region` / `#endregion`:** + +Group related test functions into named regions instead of comment dividers. +Every region must have a matching `#endregion`: + +```gdscript +#region is_equal +func test_is_equal_same_value() -> void: + assert_int(1).is_equal(1) + +func test_is_equal_different_value_fails() -> void: + assert_failure(func() -> void: assert_int(1).is_equal(2)) \ + .is_failed() +#endregion + +#region has_size +func test_has_size() -> void: + assert_array([1, 2, 3]).has_size(3) +#endregion +``` + +**Always use the type-specific assert function:** + +Match the assert to the type of the value under test. Type-specific asserts unlock +richer failure messages and type-appropriate matchers. Fall back to `assert_that` only +for custom objects and variants that have no dedicated assert. + +| Value type | Use | +| ---------- | --- | +| `bool` | `assert_bool(value)` | +| `int` | `assert_int(value)` | +| `float` | `assert_float(value)` | +| `String` | `assert_str(value)` | +| `Array` | `assert_array(value)` | +| `Dictionary` | `assert_dict(value)` | +| Custom object / variant | `assert_that(value)` | + +Pick the assert based on the type of the value โ€” whether that comes from an explicit +annotation, `:=` inference, or the return type of the called function. + +```gdscript +# Preferred โ€” assert matches the inferred type of the value +var is_alive := player.is_alive() # bool โ†’ assert_bool +assert_bool(is_alive).is_true() + +var item_count := inventory.size() # int โ†’ assert_int +assert_int(item_count).is_equal(5) + +var label := button.get_label() # String โ†’ assert_str +assert_str(label).is_equal("Start Game") + +var config := settings.to_dict() # Dictionary โ†’ assert_dict +assert_dict(config).contains_key_value("difficulty", "hard") + +var node := scene.find_child("Player") # Node โ†’ assert_that +assert_that(node).is_equal(expected_node) + +# Avoid โ€” assert_that used where a type-specific assert exists +assert_that(player.is_alive()).is_equal(true) +assert_that(inventory.size()).is_equal(5) +assert_that(button.get_label()).is_equal("Start Game") +``` + +**Fluent chain โ€” break only when necessary:** + +Keep the chain on one line when it fits within the 140-character limit and uses a single +validation call. Break with `\` continuation when the line would be too long, or when +chaining more than one validation method. + +```gdscript +# Preferred โ€” fits on one line, single validation +assert_bool(player.is_alive()).is_true() +assert_str(player.name()).is_equal("Hero") + +# Preferred โ€” break because the line would exceed 140 characters +assert_str(player.get_full_description()) \ + .is_equal("Hero the Brave, level 42, wielder of the Sword of Destiny") + +# Preferred โ€” break because multiple validations are chained +assert_str(player.name()) \ + .is_not_empty() \ + .starts_with("H") \ + .is_equal("Hero") + +# Avoid โ€” unnecessary break for a short single-validation chain +assert_bool(player.is_alive()) \ + .is_true() +``` + +**Core assert functions and chaining:** + +```gdscript +# Primitives +assert_bool(value).is_true() +assert_bool(value).is_false() + +assert_int(value).is_equal(42) +assert_int(value).is_not_equal(0) +assert_int(value).is_greater(10).is_less(100) + +assert_float(value).is_equal_approx(3.14, 0.001) + +assert_str(value).is_equal("expected") +assert_str(value).is_not_null().has_length(5).starts_with("ab").ends_with("cd").contains("bc") +assert_str(value).is_empty() + +# Objects / variants +assert_object(value).is_not_null() +assert_object(value).is_instanceof(MyClass) +assert_that(value).is_null() +assert_that(value).is_equal(expected) +assert_that(value).is_instanceof(Node) +``` + +**Object equality โ€” prefer whole-object comparison:** + +`assert_that(obj).is_equal(expected)` uses deep property comparison (`GdObjects.equals`), so +always prefer it over asserting fields one by one. Field-by-field assertions are harder to +read, require more maintenance, and produce worse failure messages. + +```gdscript +# Preferred โ€” compare the full result to an expected object in one assertion +var result := Player.new("Hero", 100, Vector3(1, 2, 3)) +assert_that(result).is_equal(Player.new("Hero", 100, Vector3(1, 2, 3))) + +# Avoid โ€” noisy, fragile, easy to miss a property +assert_str(result.name).is_equal("Hero") +assert_int(result.health).is_equal(100) +assert_that(result.position).is_equal(Vector3(1, 2, 3)) +``` + +Only fall back to field-by-field when you intentionally want to verify a single property +in isolation, or when the expected object cannot be constructed easily. + +**Inline expected values โ€” avoid unnecessary variables:** + +Put the expected value directly in the assertion unless the line would exceed the 140-character +limit. An extra `var expected` adds noise without adding clarity. + +```gdscript +# Preferred โ€” expected value inline +assert_str(player.name()).is_equal("Hero") +assert_that(result).is_equal(Player.new("Hero", 100, Vector3(1, 2, 3))) + +# Accepted โ€” variable needed to stay within the 140-character line limit +var expected := Player.new( + "Hero", 100, Vector3(1, 2, 3), ["sword", "shield"], {"speed": 5.0} +) +assert_that(result).is_equal(expected) + +# Avoid โ€” variable adds no value when the expression fits on one line +var expected_name := "Hero" +assert_str(player.name()).is_equal(expected_name) +``` + +```gdscript +# Arrays +assert_array(value).is_not_empty() +assert_array(value).has_size(3).contains(1, 2) +assert_array(value).contains_exactly(1, 2, 3) + +# Dictionaries +assert_dict(value).is_not_empty() +assert_dict(value).contains_key("foo") +assert_dict(value).contains_key_value("foo", "bar") +assert_dict(value).has_size(3).contains_key_value("foo", "bar") +``` + +**Testing expected failures:** + +```gdscript +assert_failure(func() -> void: assert_str("abc").is_equal("xyz")) \ + .is_failed() \ + .has_message("Expecting:\n 'xyz'\n but was\n 'abc'") + +assert_failure(func() -> void: assert_str("abc").is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: ''") +``` + +**Mocking and spying:** + +```gdscript +var mock :Variant = mock(MyClass) +when(mock.my_method(any_int())).thenReturn(42) +verify(mock).my_method(42) +verify(mock, times(2)).my_method(any_int()) +``` + +**Scene runner:** + +```gdscript +var runner := scene_runner("res://my_scene.tscn") +await runner.simulate_frames(10) +assert_signal(runner).is_emitted("my_signal") +runner.simulate_key_pressed(KEY_ENTER) +``` + +**Auto-free resources:** + +```gdscript +var node := auto_free(MyNode.new()) # freed after test automatically +``` diff --git a/addons/gdUnit4/test/GdUnitAwaiterTest.gd b/addons/gdUnit4/test/GdUnitAwaiterTest.gd new file mode 100644 index 0000000..d3f4084 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitAwaiterTest.gd @@ -0,0 +1,85 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitAwaiterTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/GdUnitAwaiter.gd' + +signal test_signal_a() +signal test_signal_b() +signal test_signal_c(value :String) + + +func after_test() -> void: + for node in get_children(): + if node is Timer: + remove_child(node) + (node as Timer).stop() + node.free() + + +func install_signal_emitter(signal_name :String, signal_args: Array = [], time_out : float = 0.020) -> void: + var timer := Timer.new() + add_child(timer) + timer.timeout.connect(Callable(self, "emit_test_signal").bind(signal_name, signal_args)) + timer.one_shot = true + timer.start(time_out) + + +func emit_test_signal(signal_name :String, signal_args: Array) -> void: + match signal_args.size(): + 0: emit_signal(signal_name) + 1: emit_signal(signal_name, signal_args[0]) + 2: emit_signal(signal_name, signal_args[0], signal_args[1]) + 3: emit_signal(signal_name, signal_args[0], signal_args[1], signal_args[2]) + + +func test_await_signal_on() -> void: + install_signal_emitter(test_signal_a.get_name()) + await await_signal_on(self, "test_signal_a", [], 100) + + install_signal_emitter(test_signal_b.get_name()) + await await_signal_on(self, "test_signal_b", [], 100) + + install_signal_emitter(test_signal_c.get_name(), []) + await await_signal_on(self, "test_signal_c", [], 100) + + install_signal_emitter(test_signal_c.get_name(), ["abc"]) + await await_signal_on(self, "test_signal_c", ["abc"], 100) + + install_signal_emitter(test_signal_c.get_name(), ["abc", "eee"]) + await await_signal_on(self, "test_signal_c", ["abc", "eee"], 100) + + +func test_await_signal_on_manysignals_emitted() -> void: + # emits many different signals + install_signal_emitter("test_signal_a") + install_signal_emitter("test_signal_a") + install_signal_emitter("test_signal_a") + install_signal_emitter("test_signal_c", ["xxx"]) + # the signal we want to wait for + install_signal_emitter("test_signal_c", ["abc"], .200) + install_signal_emitter("test_signal_c", ["yyy"], .100) + # we only wait for 'test_signal_c("abc")' is emitted + await await_signal_on(self, "test_signal_c", ["abc"], 300) + + +func test_await_signal_on_never_emitted_timedout() -> void: + ( + # we wait for 'test_signal_c("yyy")' which is never emitted + await assert_failure_await(func x() -> void: await await_signal_on(self, "test_signal_c", ["yyy"], 200)) + ).has_line(72)\ + .has_message("await_signal_on(test_signal_c, [\"yyy\"]) timed out after 200ms") + + +func test_await_signal_on_invalid_source_timedout() -> void: + ( + # we wait for a signal on a already freed instance + await assert_failure_await(func x() -> void: await await_signal_on(invalid_node(), "tree_entered", [], 300)) + ).has_line(80).has_message(GdAssertMessages.error_await_signal_on_invalid_instance(null, "tree_entered", [])) + + +func invalid_node() -> Node: + return null diff --git a/addons/gdUnit4/test/GdUnitAwaiterTest.gd.uid b/addons/gdUnit4/test/GdUnitAwaiterTest.gd.uid new file mode 100644 index 0000000..591d3d3 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitAwaiterTest.gd.uid @@ -0,0 +1 @@ +uid://ceocrcbp28w8g diff --git a/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd b/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd new file mode 100644 index 0000000..617f351 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd @@ -0,0 +1,12 @@ +extends GdUnitTestSuite + + +@warning_ignore("unsafe_method_access") +func test_load_performance() -> void: + var time := LocalTime.now() + prints("Scan for test suites.") + var test_suites := GdUnitTestSuiteScanner.new().scan("res://addons/gdUnit4/test/") + assert_int(time.elapsed_since_ms())\ + .override_failure_message("Expecting the loading time overall is less than 5s")\ + .is_less(5*1000) + prints("Scanning of %d test suites took" % test_suites.size(), time.elapsed_since()) diff --git a/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd.uid b/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd.uid new file mode 100644 index 0000000..86b6203 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd.uid @@ -0,0 +1 @@ +uid://crs2wuyi2yaej diff --git a/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd b/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd new file mode 100644 index 0000000..9bca95e --- /dev/null +++ b/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd @@ -0,0 +1,29 @@ +# This test verifys only the scanner functionallity +# to find all given tests by pattern 'func test_():' +extends GdUnitTestSuite + + +func test_example() -> void: + assert_str("This is an example message").has_length(26) + assert_str("This is an example message").starts_with("This is an ex") + + +func test_b() -> void: + pass + + +# test name starts with same name e.g. test_b vs test_b2 +func test_b2() -> void: + pass + + +# test scanning success with invalid formatting +func test_b21 ( ) -> void: + pass + + +# finally verify all tests are found +func after() -> void: + assert_array(get_children())\ + .extract("test_name")\ + .contains_exactly(["test_example", "test_b", "test_b2", "test_b21"]) diff --git a/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd.uid b/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd.uid new file mode 100644 index 0000000..eee425d --- /dev/null +++ b/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd.uid @@ -0,0 +1 @@ +uid://4fwha2fkarr1 diff --git a/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd b/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd new file mode 100644 index 0000000..7bd536d --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd @@ -0,0 +1,125 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite + +const SECOND :int = 1000 +const MINUTE :int = SECOND*60 + +var _before_arg :String +var _test_arg :String + + +func before() -> void: + # use some variables to test clone test suite works as expected + _before_arg = "---before---" + + +func before_test() -> void: + # set failing test to success if failed by timeout + discard_error_interupted_by_timeout() + _test_arg = "abc" + + +# without custom timeout should execute the complete test +func test_timeout_after_test_completes() -> void: + assert_str(_before_arg).is_equal("---before---") + var counter := 0 + await await_millis(1000) + prints("A","1s") + counter += 1 + await await_millis(1000) + prints("A","2s") + counter += 1 + await await_millis(1000) + prints("A","3s") + counter += 1 + await await_millis(1000) + prints("A","5s") + counter += 2 + prints("A","end test test_timeout_after_test_completes") + assert_int(counter).is_equal(5) + + +# set test timeout to 2s +@warning_ignore("unused_parameter") +func test_timeout_2s(timeout:=2000) -> void: + assert_str(_before_arg).is_equal("---before---") + prints("B", "0s") + await await_millis(1000) + prints("B", "1s") + await await_millis(1000) + prints("B", "2s") + await await_millis(1000) + # this line should not reach if timeout aborts the test case after 2s + fail("The test case must be interupted by a timeout after 2s") + prints("B", "3s") + prints("B", "end") + + +# set test timeout to 4s +@warning_ignore("unused_parameter") +func test_timeout_4s(timeout:=4000) -> void: + assert_str(_before_arg).is_equal("---before---") + prints("C", "0s") + await await_millis(1000) + prints("C", "1s") + await await_millis(1000) + prints("C", "2s") + await await_millis(1000) + prints("C", "3s") + await await_millis(4000) + # this line should not reach if timeout aborts the test case after 4s + fail("The test case must be interupted by a timeout after 4s") + prints("C", "7s") + prints("C", "end") + + +@warning_ignore("unused_parameter") +func test_timeout_single_yield_wait(timeout:=3000) -> void: + assert_str(_before_arg).is_equal("---before---") + prints("D", "0s") + await await_millis(6000) + prints("D", "6s") + # this line should not reach if timeout aborts the test case after 3s + fail("The test case must be interupted by a timeout after 3s") + prints("D", "end test test_timeout") + + +@warning_ignore("unused_parameter") +func test_timeout_long_running_test_abort(timeout:=4000) -> void: + assert_str(_before_arg).is_equal("---before---") + prints("E", "0s") + var start_time := Time.get_ticks_msec() + var sec_start_time := Time.get_ticks_msec() + + # simulate long running function + while true: + var elapsed_time := Time.get_ticks_msec() - start_time + var sec_time := Time.get_ticks_msec() - sec_start_time + + if sec_time > 1000: + sec_start_time = Time.get_ticks_msec() + prints("E", LocalTime.elapsed(elapsed_time)) + + # give system time to check for timeout + await await_millis(200) + + # exit while after 4500ms inclusive 500ms offset + if elapsed_time > 4500: + break + + # this line should not reach if timeout aborts the test case after 4s + fail("The test case must be abort interupted by a timeout 4s") + prints("F", "end test test_timeout") + + +@warning_ignore("unused_parameter", "unused_variable") +func test_timeout_fuzzer(fuzzer := Fuzzers.rangei(-23, 22), timeout:=2000) -> void: + discard_error_interupted_by_timeout() + fuzzer.next_value() + # wait each iteration 200ms + await await_millis(200) + # we expects the test is interupped after 10 iterations because each test takes 200ms + # and the test should not longer run than 2000ms + assert_int(fuzzer.iteration_index())\ + .override_failure_message("The test must be interupted after around 10 iterations")\ + .is_less_equal(10) diff --git a/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd.uid b/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd.uid new file mode 100644 index 0000000..a9752a9 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd.uid @@ -0,0 +1 @@ +uid://cwn1ond6ipft6 diff --git a/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd b/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd new file mode 100644 index 0000000..e00c249 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd @@ -0,0 +1,96 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite + +var _iteration_timer_start := 0 +var _test_values_current :Dictionary +var _test_values_expected :Dictionary + +const SECOND:int = 1000 +const MINUTE:int = SECOND*60 + +class TestCaseStatistics: + var _test_before_calls :int + var _test_after_calls :int + + + func _init(before_calls := 0, after_calls := 0) -> void: + _test_before_calls = before_calls + _test_after_calls = after_calls + + +func before() -> void: + _test_values_current = { + "test_2s" : TestCaseStatistics.new(), + "test_multi_yielding" : TestCaseStatistics.new(), + "test_multi_yielding_with_fuzzer" : TestCaseStatistics.new() + } + _test_values_expected = { + "test_2s" : TestCaseStatistics.new(1, 1), + "test_multi_yielding" : TestCaseStatistics.new(1, 1), + "test_multi_yielding_with_fuzzer" : TestCaseStatistics.new(5 , 5) + } + + +func after() -> void: + for test_case :String in _test_values_expected.keys(): + var current: TestCaseStatistics = _test_values_current[test_case] + var expected: TestCaseStatistics = _test_values_expected[test_case] + assert_int(current._test_before_calls)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [expected._test_before_calls, current._test_before_calls, test_case])\ + .is_equal(expected._test_before_calls) + assert_int(current._test_after_calls)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [expected._test_before_calls, current._test_before_calls, test_case])\ + .is_equal(expected._test_after_calls) + + +func before_test() -> void: + var current: TestCaseStatistics = _test_values_current[__active_test_case] + current._test_before_calls +=1 + + +func after_test() -> void: + var current: TestCaseStatistics = _test_values_current[__active_test_case] + current._test_after_calls +=1 + + +func test_2s() -> void: + var timer_start := Time.get_ticks_msec() + await await_millis(2000) + # subtract an offset of 100ms because the time is not accurate + assert_int(Time.get_ticks_msec()-timer_start).is_between(2*SECOND-100, 2*SECOND+100) + + +func test_multi_yielding() -> void: + var timer_start := Time.get_ticks_msec() + prints("test_yielding") + await get_tree().process_frame + prints("4") + await get_tree().create_timer(1.0).timeout + prints("3") + await get_tree().create_timer(1.0).timeout + prints("2") + await get_tree().create_timer(1.0).timeout + prints("1") + await get_tree().create_timer(1.0).timeout + prints("Go") + assert_int(Time.get_ticks_msec()-timer_start).is_greater_equal(4*(SECOND-50)) + + +func test_multi_yielding_with_fuzzer(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations := 5) -> void: + if fuzzer.iteration_index() > 5: + fail("Should only called 5 times") + if fuzzer.iteration_index() == 1: + _iteration_timer_start = Time.get_ticks_msec() + prints("test iteration %d" % fuzzer.iteration_index()) + prints("4") + await get_tree().create_timer(1.0).timeout + prints("3") + await get_tree().create_timer(1.0).timeout + prints("2") + await get_tree().create_timer(1.0).timeout + prints("1") + await get_tree().create_timer(1.0).timeout + prints("Go") + if fuzzer.iteration_index() == 5: + # elapsed time must be fuzzer_iterations times * 4s = 40s (using 3,9s because of inaccurate timings) + assert_int(Time.get_ticks_msec()-_iteration_timer_start).is_greater_equal(3900*fuzzer_iterations) diff --git a/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd.uid b/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd.uid new file mode 100644 index 0000000..8d52fbc --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd.uid @@ -0,0 +1 @@ +uid://bhgq1yrkfoxg diff --git a/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd b/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd new file mode 100644 index 0000000..21cbde1 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd @@ -0,0 +1,16 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitTestResourceLoaderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/test/GdUnitTestResourceLoader.gd' + + +func test_load_test_suite_gd() -> void: + var resource_path := "res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource" + var test_suite := GdUnitTestResourceLoader.load_test_suite_gd(resource_path) + assert_that(test_suite).is_not_null() + assert_object(test_suite).is_instanceof(GdUnitTestSuite) + test_suite.free() diff --git a/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd.uid b/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd.uid new file mode 100644 index 0000000..2e3d1f5 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd.uid @@ -0,0 +1 @@ +uid://dmurkkjrh7ceo diff --git a/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd b/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd new file mode 100644 index 0000000..9aeb861 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd @@ -0,0 +1,13 @@ +extends GdUnitTestSuite + + +func test_testsuite_loading_performance() -> void: + var time := LocalTime.now() + var reload_counter := 100.0 + for i in range(1, reload_counter): + ResourceLoader.load("res://addons/gdUnit4/src/GdUnitTestSuite.gd", "GDScript", ResourceLoader.CACHE_MODE_IGNORE) + var error_message := "Expecting the loading time of test-suite is less than 50ms\n But was %s" % (time.elapsed_since_ms() / reload_counter) + assert_float(time.elapsed_since_ms()/ reload_counter)\ + .override_failure_message(error_message)\ + .is_less(50) + prints("loading takes %d ms" % (time.elapsed_since_ms() / reload_counter)) diff --git a/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd.uid b/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd.uid new file mode 100644 index 0000000..e2e89a5 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd.uid @@ -0,0 +1 @@ +uid://csjuwxbvnnxt7 diff --git a/addons/gdUnit4/test/GdUnitTestSuiteTest.gd b/addons/gdUnit4/test/GdUnitTestSuiteTest.gd new file mode 100644 index 0000000..7baa342 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestSuiteTest.gd @@ -0,0 +1,92 @@ +# GdUnit generated TestSuite +class_name GdUnitTestSuiteTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/GdUnitTestSuite.gd' + +var _events :Array[GdUnitEvent] = [] +var _retry_count := 0 +var _test_unknown_argument_in_test_case_is_called := false + + +func collect_report(event :GdUnitEvent) -> void: + _events.push_back(event) + + +func before() -> void: + # register to receive test reports + GdUnitSignals.instance().gdunit_event.connect(collect_report) + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_CHECK, true) + + +func after() -> void: + # verify the test case `test_unknown_argument_in_test_case` was skipped + assert_bool(_test_unknown_argument_in_test_case_is_called)\ + .override_failure_message("Expecting 'test_unknown_argument_in_test_case' is skipped!")\ + .is_false() + GdUnitSignals.instance().gdunit_event.disconnect(collect_report) + + +func test_assert_that_types() -> void: + assert_object(assert_that(true)).is_instanceof(GdUnitBoolAssert) + assert_object(assert_that(1)).is_instanceof(GdUnitIntAssert) + assert_object(assert_that(3.12)).is_instanceof(GdUnitFloatAssert) + assert_object(assert_that("abc")).is_instanceof(GdUnitStringAssert) + assert_object(assert_that(Vector2.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector2i.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector3.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector3i.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector4.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector4i.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that([])).is_instanceof(GdUnitArrayAssert) + assert_object(assert_that({})).is_instanceof(GdUnitDictionaryAssert) + assert_object(assert_that(GdUnitResult.new())).is_instanceof(GdUnitObjectAssert) + # all not a built-in types mapped to default GdUnitAssert + assert_object(assert_that(Color.RED)).is_instanceof(GdUnitAssertImpl) + assert_object(assert_that(Plane.PLANE_XY)).is_instanceof(GdUnitAssertImpl) + + +func test_unknown_argument_in_test_case(_invalid_arg :int) -> void: + _test_unknown_argument_in_test_case_is_called = true + fail("This test case should be not executed, it must be skipped.") + + +func test_find_child() -> void: + var node_a :Node3D = auto_free(Node3D.new()) + node_a.set_name("node_a") + var node_b :Node3D = auto_free(Node3D.new()) + node_b.set_name("node_b") + var node_c :Node3D = auto_free(Node3D.new()) + node_c.set_name("node_c") + add_child(node_a, true) + node_a.add_child(node_b, true) + node_b.add_child(node_c, true) + + assert_object(find_child("node_a", true, false)).is_same(node_a) + assert_object(find_child("node_b", true, false)).is_same(node_b) + assert_object(find_child("node_c", true, false)).is_same(node_c) + + +func test_find_by_path() -> void: + var node_a :Node3D = auto_free(Node3D.new()) + node_a.set_name("node_a") + var node_b :Node3D = auto_free(Node3D.new()) + node_b.set_name("node_b") + var node_c :Node3D = auto_free(Node3D.new()) + node_c.set_name("node_c") + add_child(node_a, true) + node_a.add_child(node_b, true) + node_b.add_child(node_c, true) + + assert_object(get_node(node_a.get_path())).is_same(node_a) + assert_object(get_node(node_b.get_path())).is_same(node_b) + assert_object(get_node(node_c.get_path())).is_same(node_c) + + +func test_flaky_success() -> void: + _retry_count += 1 + # do fail on first two retries + if _retry_count <= 2: + fail("failure 1: at retry %d" % _retry_count) + fail("failure 2: at retry %d" % _retry_count) diff --git a/addons/gdUnit4/test/GdUnitTestSuiteTest.gd.uid b/addons/gdUnit4/test/GdUnitTestSuiteTest.gd.uid new file mode 100644 index 0000000..f2c91b4 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestSuiteTest.gd.uid @@ -0,0 +1 @@ +uid://cahhreh5jm8po diff --git a/addons/gdUnit4/test/GdUnitTupleTest.gd b/addons/gdUnit4/test/GdUnitTupleTest.gd new file mode 100644 index 0000000..4c90b3c --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTupleTest.gd @@ -0,0 +1,22 @@ +extends GdUnitTestSuite + + +func test_tuple_empty() -> void: + var t := GdUnitTuple.new() + assert_array(t.values()).is_empty() + + +func test_tuple_construct() -> void: + assert_array(GdUnitTuple.new(0, 1).values()).contains_exactly(0, 1) + assert_array(GdUnitTuple.new(0, 1, 2).values()).contains_exactly(0, 1, 2) + assert_array(GdUnitTuple.new(0, 1, 2, 3).values()).contains_exactly(0, 1, 2, 3) + assert_array(GdUnitTuple.new(0, 1, 2, 3, "abc").values()).contains_exactly(0, 1, 2, 3, "abc") + assert_array(GdUnitTuple.new([0, 1, 2, 3], "abc").values()).contains_exactly([0, 1, 2, 3], "abc") + + +func test_tuple_of() -> void: + assert_array(tuple(0, 1).values()).contains_exactly(0, 1) + assert_array(tuple(0, 1, 2).values()).contains_exactly(0, 1, 2) + assert_array(tuple(0, 1, 2, 3).values()).contains_exactly(0, 1, 2, 3) + assert_array(tuple(0, 1, 2, 3, "abc").values()).contains_exactly(0, 1, 2, 3, "abc") + assert_array(tuple([0, 1, 2, 3], "abc").values()).contains_exactly([0, 1, 2, 3], "abc") diff --git a/addons/gdUnit4/test/GdUnitTupleTest.gd.uid b/addons/gdUnit4/test/GdUnitTupleTest.gd.uid new file mode 100644 index 0000000..39fa5d3 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTupleTest.gd.uid @@ -0,0 +1 @@ +uid://duw84tlnkolds diff --git a/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd b/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd new file mode 100644 index 0000000..1698977 --- /dev/null +++ b/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd @@ -0,0 +1,23 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name CallBackValueProviderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/CallBackValueProvider.gd' + + +func next_value() -> String: + return "a value" + + +func test_get_value() -> void: + var vp := CallBackValueProvider.new(self, "next_value") + assert_str(await vp.get_value()).is_equal("a value") + + +func test_construct_invalid() -> void: + var vp := CallBackValueProvider.new(self, "invalid_func", Array(), false) + # will return null because of invalid function name + assert_str(await vp.get_value()).is_null() diff --git a/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd.uid b/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd.uid new file mode 100644 index 0000000..f04fc1b --- /dev/null +++ b/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd.uid @@ -0,0 +1 @@ +uid://cmnqauo285k7p diff --git a/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd new file mode 100644 index 0000000..31c2be1 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd @@ -0,0 +1,1155 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd' + +func before() -> void: + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_WARNINGS, false) + + +func test_is_null() -> void: + assert_array(null).is_null() + + assert_failure(func() -> void: assert_array([]).is_null()) \ + .is_failed() \ + .has_message("Expecting: '' but was ''") + + +func test_is_not_null() -> void: + assert_array([]).is_not_null() + + assert_failure(func() -> void: assert_array(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_array([1, 2, 3, 4, 2, 5]).is_equal([1, 2, 3, 4, 2, 5]) + # should fail because the array not contains same elements and has diff size + assert_failure(func() -> void: assert_array([1, 2, 4, 5]).is_equal([1, 2, 3, 4, 2, 5])) \ + .is_failed() + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).is_equal([1, 2, 3, 4])) \ + .is_failed() + # current array is bigger than expected + assert_failure(func() -> void: assert_array([1, 2222, 3, 4, 5, 6]).is_equal([1, 2, 3, 4])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4]' + but was + '[1, 2222, 3, 4, 5, 6]' + + Differences found: + Index Current Expected 1 2222 2 4 5 5 6 """ + .dedent().trim_prefix("\n")) + + # expected array is bigger than current + assert_failure(func() -> void: assert_array([1, 222, 3, 4]).is_equal([1, 2, 3, 4, 5, 6])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5, 6]' + but was + '[1, 222, 3, 4]' + + Differences found: + Index Current Expected 1 222 2 4 5 5 6 """ + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).is_equal([1, 2, 3])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3]' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_equal_variadic_args() -> void: + assert_array([1, 2, 3, 4, 2, 5]).is_equal(1, 2, 3, 4, 2, 5) + # should fail because the array not contains same elements and has diff size + assert_failure(func() -> void: assert_array([1, 2, 4, 5]).is_equal(1, 2, 3, 4, 2, 5)) \ + .is_failed() + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).is_equal(1, 2, 3, 4)) \ + .is_failed() + # current array is bigger than expected + assert_failure(func() -> void: assert_array([1, 2222, 3, 4, 5, 6]).is_equal(1, 2, 3, 4)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4]' + but was + '[1, 2222, 3, 4, 5, 6]' + + Differences found: + Index Current Expected 1 2222 2 4 5 5 6 """ + .dedent().trim_prefix("\n")) + + # expected array is bigger than current + assert_failure(func() -> void: assert_array([1, 222, 3, 4]).is_equal(1, 2, 3, 4, 5, 6)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5, 6]' + but was + '[1, 222, 3, 4]' + + Differences found: + Index Current Expected 1 222 2 4 5 5 6 """ + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).is_equal(1, 2, 3)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3]' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_equal_big_arrays() -> void: + var expeted := Array() + expeted.resize(1000) + for i in 1000: + expeted[i] = i + var current := expeted.duplicate() + current[10] = "invalid" + current[40] = "invalid" + current[100] = "invalid" + current[888] = "invalid" + + assert_failure(func() -> void: assert_array(current).is_equal(expeted)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, ...]' + but was + '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, invalid, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, ...]' + + Differences found: + Index Current Expected 10 invalid 10 40 invalid 40 100 invalid 100 888 invalid 888 """ + .dedent().trim_prefix("\n")) + + +func test_is_equal_ignoring_case() -> void: + assert_array(["this", "is", "a", "message"]).is_equal_ignoring_case(["This", "is", "a", "Message"]) + # should fail because the array not contains same elements + assert_failure(func() -> void: assert_array(["this", "is", "a", "message"]).is_equal_ignoring_case(["This", "is", "an", "Message"])) \ + .is_failed() + assert_failure(func() -> void: assert_array(null).is_equal_ignoring_case(["This", "is"])) \ + .is_failed() \ + .has_message(""" + Expecting: + '["This", "is"]' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_equal_ignoring_case_variadic_args() -> void: + assert_array(["this", "is", "a", "message"]).is_equal_ignoring_case("This", "is", "a", "Message") + # should fail because the array not contains same elements + assert_failure(func() -> void: assert_array(["this", "is", "a", "message"]).is_equal_ignoring_case("This", "is", "an", "Message")) \ + .is_failed() + assert_failure(func() -> void: assert_array(null).is_equal_ignoring_case(["This", "is"])) \ + .is_failed() \ + .has_message(""" + Expecting: + '["This", "is"]' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_not_equal() -> void: + assert_array(null).is_not_equal([1, 2, 3]) + assert_array([1, 2, 3, 4, 5]).is_not_equal([1, 2, 3, 4, 5, 6]) + # should fail because the array contains same elements + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).is_not_equal([1, 2, 3, 4, 5])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + not equal to + '[1, 2, 3, 4, 5]'""" + .dedent().trim_prefix("\n")) + + +func test_is_not_equal_variadic_args() -> void: + assert_array(null).is_not_equal(1, 2, 3) + assert_array([1, 2, 3, 4, 5]).is_not_equal(1, 2, 3, 4, 5, 6) + # should fail because the array contains same elements + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).is_not_equal(1, 2, 3, 4, 5)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + not equal to + '[1, 2, 3, 4, 5]'""" + .dedent().trim_prefix("\n")) + + +func test_is_not_equal_ignoring_case() -> void: + assert_array(null).is_not_equal_ignoring_case(["This", "is", "an", "Message"]) + assert_array(["this", "is", "a", "message"]).is_not_equal_ignoring_case(["This", "is", "an", "Message"]) + # should fail because the array contains same elements ignoring case sensitive + assert_failure(func() -> void: assert_array(["this", "is", "a", "message"]).is_not_equal_ignoring_case(["This", "is", "a", "Message"])) \ + .is_failed() \ + .has_message(""" + Expecting: + '["This", "is", "a", "Message"]' + not equal to (case insensitiv) + '["this", "is", "a", "message"]'""" + .dedent().trim_prefix("\n")) + + +func test_is_not_equal_ignoring_case_varadic_args() -> void: + assert_array(null).is_not_equal_ignoring_case("This", "is", "an", "Message") + assert_array(["this", "is", "a", "message"]).is_not_equal_ignoring_case("This", "is", "an", "Message") + # should fail because the array contains same elements ignoring case sensitive + assert_failure(func() -> void: assert_array(["this", "is", "a", "message"]).is_not_equal_ignoring_case("This", "is", "a", "Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + '["This", "is", "a", "Message"]' + not equal to (case insensitiv) + '["this", "is", "a", "message"]'""" + .dedent().trim_prefix("\n")) + + +func test_is_empty() -> void: + assert_array([]).is_empty() + + assert_failure(func() -> void: assert_array([1, 2, 3]).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + '[1, 2, 3]'""" + .dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_array(null).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_not_empty() -> void: + assert_array(null).is_not_empty() + assert_array([1]).is_not_empty() + + assert_failure(func() -> void: assert_array([]).is_not_empty()) \ + .is_failed() \ + .has_message("Expecting:\n must not be empty") + + +func test_is_same() -> void: + var value := [0] + assert_array(value).is_same(value) + + assert_failure(func() -> void: assert_array(value).is_same(value.duplicate()))\ + .is_failed()\ + .has_message("Expecting:\n '[0]'\n to refer to the same object\n '[0]'") + + +func test_is_not_same() -> void: + assert_array([0]).is_not_same([0]) + var value := [0] + assert_failure(func() -> void: assert_array(value).is_not_same(value))\ + .is_failed()\ + .has_message("Expecting not same:\n '[0]'") + + +func test_has_size() -> void: + assert_array([1, 2, 3, 4, 5]).has_size(5) + assert_array(["a", "b", "c", "d", "e", "f"]).has_size(6) + + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).has_size(4)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '4' + but was + '5'""" + .dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_array(null).has_size(4)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '4' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_contains() -> void: + assert_array([1, 2, 3, 4, 5]).contains([]) + assert_array([1, 2, 3, 4, 5]).contains([5, 2]) + assert_array([1, 2, 3, 4, 5]).contains([5, 4, 3, 2, 1]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains([TestObj.new("A", 0)]) + + # should fail because the array not contains 7 and 6 + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).contains([2, 7, 6])) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '[1, 2, 3, 4, 5]' + do contains (in any order) + '[2, 7, 6]' + but could not find elements: + '[7, 6]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).contains([2, 7, 6])) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '' + do contains (in any order) + '[2, 7, 6]' + but could not find elements: + '[2, 7, 6]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains([TestObj.new("C", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '[class:A, class:B]' + do contains (in any order) + '[class:C]' + but could not find elements: + '[class:C]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_variadic_args() -> void: + assert_array([1, 2, 3, 4, 5]).contains() + assert_array([1, 2, 3, 4, 5]).contains(5, 2) + assert_array([1, 2, 3, 4, 5]).contains(5, 4, 3, 2, 1) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains(TestObj.new("A", 0)) + + # should fail because the array not contains 7 and 6 + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).contains(2, 7, 6)) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '[1, 2, 3, 4, 5]' + do contains (in any order) + '[2, 7, 6]' + but could not find elements: + '[7, 6]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).contains(2, 7, 6)) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '' + do contains (in any order) + '[2, 7, 6]' + but could not find elements: + '[2, 7, 6]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains(TestObj.new("C", 0))) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '[class:A, class:B]' + do contains (in any order) + '[class:C]' + but could not find elements: + '[class:C]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_exactly() -> void: + assert_array([1, 2, 3, 4, 5]).contains_exactly([1, 2, 3, 4, 5]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_exactly([TestObj.new("A", 0), valueB]) + + # should fail because the array contains the same elements but in a different order + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).contains_exactly([1, 4, 3, 2, 5])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5]' + do contains (in same order) + '[1, 4, 3, 2, 5]' + but has different order at position '1' + '2' vs '4'""" + .dedent().trim_prefix("\n")) + + # should fail because the array contains more elements and in a different order + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5, 6, 7]).contains_exactly([1, 4, 3, 2, 5])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5, 6, 7]' + do contains (in same order) + '[1, 4, 3, 2, 5]' + but some elements where not expected: + '[6, 7]'""" + .dedent().trim_prefix("\n")) + + # should fail because the array contains less elements and in a different order + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).contains_exactly([1, 4, 3, 2, 5, 6, 7])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5]' + do contains (in same order) + '[1, 4, 3, 2, 5, 6, 7]' + but could not find elements: + '[6, 7]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).contains_exactly([1, 4, 3])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains (in same order) + '[1, 4, 3]' + but could not find elements: + '[1, 4, 3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_exactly([valueB, TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[class:A, class:B]' + do contains (in same order) + '[class:B, class:A]' + but has different order at position '0' + 'class:A' vs 'class:B'""" + .dedent().trim_prefix("\n")) + + +func test_contains_exactly_variadic_args() -> void: + assert_array([1, 2, 3, 4, 5]).contains_exactly(1, 2, 3, 4, 5) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_exactly(TestObj.new("A", 0), valueB) + + # should fail because the array contains the same elements but in a different order + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).contains_exactly(1, 4, 3, 2, 5)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5]' + do contains (in same order) + '[1, 4, 3, 2, 5]' + but has different order at position '1' + '2' vs '4'""" + .dedent().trim_prefix("\n")) + + # should fail because the array contains more elements and in a different order + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5, 6, 7]).contains_exactly(1, 4, 3, 2, 5)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5, 6, 7]' + do contains (in same order) + '[1, 4, 3, 2, 5]' + but some elements where not expected: + '[6, 7]'""" + .dedent().trim_prefix("\n")) + + # should fail because the array contains less elements and in a different order + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).contains_exactly(1, 4, 3, 2, 5, 6, 7)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5]' + do contains (in same order) + '[1, 4, 3, 2, 5, 6, 7]' + but could not find elements: + '[6, 7]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).contains_exactly(1, 4, 3)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains (in same order) + '[1, 4, 3]' + but could not find elements: + '[1, 4, 3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_exactly(valueB, TestObj.new("A", 0))) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[class:A, class:B]' + do contains (in same order) + '[class:B, class:A]' + but has different order at position '0' + 'class:A' vs 'class:B'""" + .dedent().trim_prefix("\n")) + + +func test_contains_exactly_in_any_order() -> void: + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order([1, 2, 3, 4, 5]) + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order([5, 3, 2, 4, 1]) + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order([5, 1, 2, 4, 3]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_exactly_in_any_order([valueB, TestObj.new("A", 0)]) + + # should fail because the array contains not exactly the same elements in any order + assert_failure(func() -> void: assert_array([1, 2, 6, 4, 5]).contains_exactly_in_any_order([5, 3, 2, 4, 1, 9, 10])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 6, 4, 5]' + do contains exactly (in any order) + '[5, 3, 2, 4, 1, 9, 10]' + but some elements where not expected: + '[6]' + and could not find elements: + '[3, 9, 10]'""" + .dedent().trim_prefix("\n")) + + #should fail because the array contains the same elements but in a different order + assert_failure(func() -> void: assert_array([1, 2, 6, 9, 10, 4, 5]).contains_exactly_in_any_order([5, 3, 2, 4, 1])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 6, 9, 10, 4, 5]' + do contains exactly (in any order) + '[5, 3, 2, 4, 1]' + but some elements where not expected: + '[6, 9, 10]' + and could not find elements: + '[3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).contains_exactly_in_any_order([1, 4, 3])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains exactly (in any order) + '[1, 4, 3]' + but could not find elements: + '[1, 4, 3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_exactly_in_any_order([valueB, TestObj.new("C", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[class:A, class:B]' + do contains exactly (in any order) + '[class:B, class:C]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:C]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_exactly_in_any_order_variadic_args() -> void: + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order(1, 2, 3, 4, 5) + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order(5, 3, 2, 4, 1) + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order(5, 1, 2, 4, 3) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_exactly_in_any_order(valueB, TestObj.new("A", 0)) + + # should fail because the array contains not exactly the same elements in any order + assert_failure(func() -> void: assert_array([1, 2, 6, 4, 5]).contains_exactly_in_any_order(5, 3, 2, 4, 1, 9, 10)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 6, 4, 5]' + do contains exactly (in any order) + '[5, 3, 2, 4, 1, 9, 10]' + but some elements where not expected: + '[6]' + and could not find elements: + '[3, 9, 10]'""" + .dedent().trim_prefix("\n")) + + #should fail because the array contains the same elements but in a different order + assert_failure(func() -> void: assert_array([1, 2, 6, 9, 10, 4, 5]).contains_exactly_in_any_order(5, 3, 2, 4, 1)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 6, 9, 10, 4, 5]' + do contains exactly (in any order) + '[5, 3, 2, 4, 1]' + but some elements where not expected: + '[6, 9, 10]' + and could not find elements: + '[3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array(null).contains_exactly_in_any_order(1, 4, 3)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains exactly (in any order) + '[1, 4, 3]' + but could not find elements: + '[1, 4, 3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_exactly_in_any_order(valueB, TestObj.new("C", 0))) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[class:A, class:B]' + do contains exactly (in any order) + '[class:B, class:C]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:C]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same() -> void: + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + + assert_array([valueA, valueB]).contains_same([valueA]) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_same([TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME elements: + '[class:A, class:B]' + do contains (in any order) + '[class:A]' + but could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same_variadic_args() -> void: + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + + assert_array([valueA, valueB]).contains_same(valueA) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_same(TestObj.new("A", 0))) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME elements: + '[class:A, class:B]' + do contains (in any order) + '[class:A]' + but could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same_exactly() -> void: + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_same_exactly([valueA, valueB]) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_same_exactly([valueB, valueA])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME exactly elements: + '[class:A, class:B]' + do contains (in same order) + '[class:B, class:A]' + but has different order at position '0' + 'class:A' vs 'class:B'""" + .dedent().trim_prefix("\n")) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_same_exactly([TestObj.new("A", 0), valueB])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME exactly elements: + '[class:A, class:B]' + do contains (in same order) + '[class:A, class:B]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same_exactly_in_any_order() -> void: + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_same_exactly_in_any_order([valueB, valueA]) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_same_exactly_in_any_order([valueB, TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME exactly elements: + '[class:A, class:B]' + do contains exactly (in any order) + '[class:B, class:A]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same_exactly_in_any_order_variadic_args() -> void: + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_same_exactly_in_any_order(valueB, valueA) + + assert_failure(func() -> void: assert_array([valueA, valueB]).contains_same_exactly_in_any_order(valueB, TestObj.new("A", 0))) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME exactly elements: + '[class:A, class:B]' + do contains exactly (in any order) + '[class:B, class:A]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_not_contains() -> void: + assert_array([]).not_contains([0]) + assert_array([1, 2, 3, 4, 5]).not_contains([0]) + assert_array([1, 2, 3, 4, 5]).not_contains([0, 6]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).not_contains([TestObj.new("C", 0)]) + + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).not_contains([5]))\ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[5]' + but found elements: + '[5]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).not_contains([1, 4, 6])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[1, 4, 6]' + but found elements: + '[1, 4]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).not_contains([6, 4, 1])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[6, 4, 1]' + but found elements: + '[4, 1]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func() -> void: assert_array([valueA, valueB]).not_contains([TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[class:A, class:B]' + do not contains + '[class:A]' + but found elements: + '[class:A]'""" + .dedent().trim_prefix("\n") + ) + + +func test_not_contains_variadic_args() -> void: + assert_array([]).not_contains(0) + assert_array([1, 2, 3, 4, 5]).not_contains(0) + assert_array([1, 2, 3, 4, 5]).not_contains(0, 6) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).not_contains(TestObj.new("C", 0)) + + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).not_contains(5))\ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[5]' + but found elements: + '[5]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).not_contains(1, 4, 6)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[1, 4, 6]' + but found elements: + '[1, 4]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func() -> void: assert_array([1, 2, 3, 4, 5]).not_contains(6, 4, 1)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[6, 4, 1]' + but found elements: + '[4, 1]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func() -> void: assert_array([valueA, valueB]).not_contains(TestObj.new("A", 0))) \ + .is_failed() \ + .has_message(""" + Expecting: + '[class:A, class:B]' + do not contains + '[class:A]' + but found elements: + '[class:A]'""" + .dedent().trim_prefix("\n") + ) + + +func test_not_contains_same() -> void: + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + var valueC := TestObj.new("B", 0) + assert_array([valueA, valueB]).not_contains_same([valueC]) + + assert_failure(func() -> void: assert_array([valueA, valueB]).not_contains_same([valueB])) \ + .is_failed() \ + .has_message(""" + Expecting SAME: + '[class:A, class:B]' + do not contains + '[class:B]' + but found elements: + '[class:B]'""" + .dedent().trim_prefix("\n") + ) + + +func test_not_contains_same_variadic_args() -> void: + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + var valueC := TestObj.new("B", 0) + assert_array([valueA, valueB]).not_contains_same(valueC) + + assert_failure(func() -> void: assert_array([valueA, valueB]).not_contains_same(valueB)) \ + .is_failed() \ + .has_message(""" + Expecting SAME: + '[class:A, class:B]' + do not contains + '[class:B]' + but found elements: + '[class:B]'""" + .dedent().trim_prefix("\n") + ) + + +func test_fluent() -> void: + assert_array([])\ + .has_size(0)\ + .is_empty()\ + .is_not_null()\ + .contains([])\ + .contains_exactly([]) + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func() -> void: assert_array(1)) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_array(1.3)) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_array(true)) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_array(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + + +func test_extract() -> void: + # try to extract checked base types + assert_array([1, false, 3.14, null, Color.ALICE_BLUE]).extract("get_class") \ + .contains_exactly("n.a.", "n.a.", "n.a.", null, "n.a.") + # extracting by a func without arguments + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("get_class") \ + .contains_exactly("RefCounted", "n.a.", "AStar3D", "Node") + # extracting by a func with arguments + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("has_signal", ["tree_entered"]) \ + .contains_exactly(false, "n.a.", false, true) + # extracting by a func with arguments using variadic syntax + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("has_signal", "tree_entered") \ + .contains_exactly(false, "n.a.", false, true) + + # try extract checked object via a func that not exists + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("invalid_func") \ + .contains_exactly("n.a.", "n.a.", "n.a.", "n.a.") + # try extract checked object via a func that has no return value + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("remove_meta", [""]) \ + .contains_exactly(null, "n.a.", null, null) + + assert_failure(func() -> void: assert_array(null).extract("get_class").contains_exactly("AStar3D", "Node")) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains (in same order) + '["AStar3D", "Node"]' + but could not find elements: + '["AStar3D", "Node"]'""" + .dedent().trim_prefix("\n")) + + +class TestObj: + var _name :String + var _value :Variant + var _x :Variant + + func _init(name :String, value :Variant, x :Variant = null) -> void: + _name = name + _value = value + _x = x + + func get_name() -> String: + return _name + + func get_value() -> Variant: + return _value + + func get_x() -> Variant: + return _x + + func get_x1() -> String: + return "x1" + + func get_x2() -> String: + return "x2" + + func get_x3() -> String: + return "x3" + + func get_x4() -> String: + return "x4" + + func get_x5() -> String: + return "x5" + + func get_x6() -> String: + return "x6" + + func get_x7() -> String: + return "x7" + + func get_x8() -> String: + return "x8" + + func get_x9() -> String: + return "x9" + + func _to_string() -> String: + return "class:" + _name + + +func test_extractv() -> void: + # single extract + assert_array([1, false, 3.14, null, Color.ALICE_BLUE])\ + .extractv(extr("get_class"))\ + .contains_exactly("n.a.", "n.a.", "n.a.", null, "n.a.") + # tuple of two + assert_array([TestObj.new("A", 10), TestObj.new("B", "foo"), Color.ALICE_BLUE, TestObj.new("C", 11)])\ + .extractv(extr("get_name"), extr("get_value"))\ + .contains_exactly(tuple("A", 10), tuple("B", "foo"), tuple("n.a.", "n.a."), tuple("C", 11)) + # tuple of three + assert_array([TestObj.new("A", 10), TestObj.new("B", "foo", "bar"), TestObj.new("C", 11, 42)])\ + .extractv(extr("get_name"), extr("get_value"), extr("get_x"))\ + .contains_exactly(tuple("A", 10, null), tuple("B", "foo", "bar"), tuple("C", 11, 42)) + + assert_failure(func() -> void: + assert_array(null) \ + .extractv(extr("get_name"), extr("get_value"), extr("get_x")) \ + .contains_exactly(tuple("A", 10, null), tuple("B", "foo", "bar"), tuple("C", 11, 42))) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains (in same order) + '[tuple(["A", 10, ]), tuple(["B", "foo", "bar"]), tuple(["C", 11, 42])]' + but could not find elements: + '[tuple(["A", 10, ]), tuple(["B", "foo", "bar"]), tuple(["C", 11, 42])]'""" + .dedent().trim_prefix("\n")) + + +func test_extractv_chained_func() -> void: + var root_a := TestObj.new("root_a", null) + var obj_a := TestObj.new("A", root_a) + var obj_b := TestObj.new("B", root_a) + var obj_c := TestObj.new("C", root_a) + var root_b := TestObj.new("root_b", root_a) + var obj_x := TestObj.new("X", root_b) + var obj_y := TestObj.new("Y", root_b) + + assert_array([obj_a, obj_b, obj_c, obj_x, obj_y])\ + .extractv(extr("get_name"), extr("get_value.get_name"))\ + .contains_exactly( + tuple("A", "root_a"), + tuple("B", "root_a"), + tuple("C", "root_a"), + tuple("X", "root_b"), + tuple("Y", "root_b") + ) + + +func test_extract_chained_func() -> void: + var root_a := TestObj.new("root_a", null) + var obj_a := TestObj.new("A", root_a) + var obj_b := TestObj.new("B", root_a) + var obj_c := TestObj.new("C", root_a) + var root_b := TestObj.new("root_b", root_a) + var obj_x := TestObj.new("X", root_b) + var obj_y := TestObj.new("Y", root_b) + + assert_array([obj_a, obj_b, obj_c, obj_x, obj_y])\ + .extract("get_value.get_name")\ + .contains_exactly( + "root_a", + "root_a", + "root_a", + "root_b", + "root_b", + ) + + +func test_extractv_max_args() -> void: + assert_array([TestObj.new("A", 10), TestObj.new("B", "foo", "bar"), TestObj.new("C", 11, 42)])\ + .extractv(\ + extr("get_name"), + extr("get_x1"), + extr("get_x2"), + extr("get_x3"), + extr("get_x4"), + extr("get_x5"), + extr("get_x6"), + extr("get_x7"), + extr("get_x8"), + extr("get_x9"))\ + .contains_exactly( + tuple("A", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9"), + tuple("B", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9"), + tuple("C", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9")) + + +func test_override_failure_message() -> void: + assert_object(assert_array([]).override_failure_message("error")).is_instanceof(GdUnitArrayAssert) + assert_failure(func() -> void: assert_array([]) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_array([]).append_failure_message("error")).is_instanceof(GdUnitArrayAssert) + assert_failure(func() -> void: assert_array([]) \ + .append_failure_message("custom failure data") \ + .is_not_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must not be empty + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_array([]).is_empty() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_array([]).is_not_empty()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_array([]).is_empty() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() + + +class ExampleTestClass extends RefCounted: + var _childs := Array() + var _parent :RefCounted = null + + + func add_child(child :ExampleTestClass) -> ExampleTestClass: + _childs.append(child) + child._parent = self + return self + + + func dispose() -> void: + _parent = null + _childs.clear() + + +func test_contains_exactly_stuck() -> void: + var example_a := ExampleTestClass.new()\ + .add_child(ExampleTestClass.new())\ + .add_child(ExampleTestClass.new()) + var example_b := ExampleTestClass.new()\ + .add_child(ExampleTestClass.new())\ + .add_child(ExampleTestClass.new()) + # this test was stuck and ends after a while into an aborted test case + # https://github.com/MikeSchulze/gdUnit3/issues/244 + assert_failure(func() -> void: assert_array([example_a, example_b]).contains_exactly(example_a, example_b, example_a))\ + .is_failed() + # manual free because of cross references + example_a.dispose() + example_b.dispose() diff --git a/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd.uid new file mode 100644 index 0000000..4e68686 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://csy6c4pxelypl diff --git a/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd new file mode 100644 index 0000000..8bb3393 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd @@ -0,0 +1,148 @@ +# GdUnit generated TestSuite +class_name GdUnitAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd' + + +func before() -> void: + + assert_failure(func() -> void: assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(11) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func after() -> void: + assert_failure(func() -> void: assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(18) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func before_test() -> void: + assert_failure(func() -> void: assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(25) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func after_test() -> void: + assert_failure(func() -> void: assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(32) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number() -> void: + # test to return the current line number for an failure + assert_failure(func() -> void: assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(40) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number_yielded() -> void: + # test to return the current line number after using yield + await get_tree().create_timer(0.100).timeout + assert_failure(func() -> void: assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(49) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number_multiline() -> void: + # test to return the current line number for an failure + # https://github.com/godotengine/godot/issues/43326 + assert_failure(func() -> void: assert_int(10)\ + .is_not_negative()\ + .is_equal(42)) \ + .is_failed() \ + .has_line(58) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number_verify() -> void: + var obj :Variant = mock(RefCounted) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: verify(obj, 1).get_reference_count()) \ + .is_failed() \ + .has_line(69) \ + .has_message("Expecting interaction on:\n 'get_reference_count()' 1 time's\nBut found interactions on:\n") + + +func test_is_null() -> void: + assert_that(null).is_null() + + assert_failure(func() -> void: assert_that(Color.RED).is_null()) \ + .is_failed() \ + .has_line(78) \ + .starts_with_message("Expecting: '' but was 'Color$v0'" + .replace("$v0", str(Color.RED)) + ) + + +func test_is_not_null() -> void: + assert_that(Color.RED).is_not_null() + + assert_failure(func() -> void: assert_that(null).is_not_null()) \ + .is_failed() \ + .has_line(89) \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_that(Color.RED).is_equal(Color.RED) + assert_that(Plane.PLANE_XY).is_equal(Plane.PLANE_XY) + + assert_failure(func() -> void: assert_that(Color.RED).is_equal(Color.GREEN)) \ + .is_failed() \ + .has_line(99) \ + .has_message("Expecting:\n 'Color$v0'\n but was\n 'Color$v1'" + .replace("$v0", str(Color.GREEN)) + .replace("$v1", str(Color.RED)) + ) + + +func test_is_not_equal() -> void: + assert_that(Color.RED).is_not_equal(Color.GREEN) + assert_that(Plane.PLANE_XY).is_not_equal(Plane.PLANE_XZ) + + assert_failure(func() -> void: assert_that(Color.RED).is_not_equal(Color.RED)) \ + .is_failed() \ + .has_line(112) \ + .has_message("Expecting:\n 'Color$v0'\n not equal to\n 'Color$v1'" + .replace("$v0", str(Color.RED)) + .replace("$v1", str(Color.RED)) + ) + + +func test_override_failure_message() -> void: + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_that(Color.RED) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_line(123) \ + .has_message("Custom failure message") + + +func test_assert_not_yet_implemented() -> void: + assert_failure(func() -> void: assert_not_yet_implemented()) \ + .is_failed() \ + .has_line(132) \ + .has_message("Test not implemented!") + + +func test_append_failure_message() -> void: + assert_object(assert_that(null).append_failure_message("error")).is_instanceof(GdUnitObjectAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_that(null) \ + .append_failure_message("custom failure data") \ + .is_not_null()) \ + .is_failed() \ + .has_message(""" + Expecting: not to be '' + Additional info: + custom failure data""".dedent().trim_prefix("\n")) diff --git a/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd.uid new file mode 100644 index 0000000..0f728d8 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://cy66pjih4gxf8 diff --git a/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd new file mode 100644 index 0000000..0e8a18f --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd @@ -0,0 +1,131 @@ +# GdUnit generated TestSuite +class_name GdUnitBoolAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd' + + +func test_is_true() -> void: + assert_bool(true).is_true() + + assert_failure(func() -> void: assert_bool(false).is_true())\ + .is_failed() \ + .has_message("Expecting: 'true' but is 'false'") + assert_failure(func() -> void: assert_bool(null).is_true()) \ + .is_failed() \ + .has_message("Expecting: 'true' but is ''") + + +func test_isFalse() -> void: + assert_bool(false).is_false() + + assert_failure(func() -> void: assert_bool(true).is_false()) \ + .is_failed() \ + .has_message("Expecting: 'false' but is 'true'") + assert_failure(func() -> void: assert_bool(null).is_false()) \ + .is_failed() \ + .has_message("Expecting: 'false' but is ''") + + +func test_is_null() -> void: + assert_bool(null).is_null() + # should fail because the current is not null + assert_failure(func() -> void: assert_bool(true).is_null())\ + .is_failed() \ + .starts_with_message("Expecting: '' but was 'true'") + + +func test_is_not_null() -> void: + assert_bool(true).is_not_null() + # should fail because the current is null + assert_failure(func() -> void: assert_bool(null).is_not_null())\ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_bool(true).is_equal(true) + assert_bool(false).is_equal(false) + + assert_failure(func() -> void: assert_bool(true).is_equal(false)) \ + .is_failed() \ + .has_message("Expecting:\n 'false'\n but was\n 'true'") + assert_failure(func() -> void: assert_bool(null).is_equal(false)) \ + .is_failed() \ + .has_message("Expecting:\n 'false'\n but was\n ''") + + +func test_is_not_equal() -> void: + assert_bool(null).is_not_equal(false) + assert_bool(true).is_not_equal(false) + assert_bool(false).is_not_equal(true) + + assert_failure(func() -> void: assert_bool(true).is_not_equal(true)) \ + .is_failed() \ + .has_message("Expecting:\n 'true'\n not equal to\n 'true'") + + +func test_fluent() -> void: + assert_bool(true).is_true().is_equal(true).is_not_equal(false) + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func() -> void: assert_bool(1)) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_bool(3.13)) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_bool("foo")) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_bool(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_object(assert_bool(true).override_failure_message("error")).is_instanceof(GdUnitBoolAssert) + assert_failure(func() -> void: assert_bool(true) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_bool(true).append_failure_message("error")).is_instanceof(GdUnitBoolAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_bool(true) \ + .append_failure_message("custom failure data") \ + .is_false()) \ + .is_failed() \ + .has_message(""" + Expecting: 'false' but is 'true' + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_bool(true).is_true() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_bool(true).is_false()).is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_bool(true).is_true() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd.uid new file mode 100644 index 0000000..a031cf0 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://b36l701yhb08m diff --git a/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd new file mode 100644 index 0000000..3362cae --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd @@ -0,0 +1,598 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd' + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func() -> void: assert_dict(1)) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_dict(1.3)) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_dict(true)) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_dict("abc")) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_dict([])) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_dict(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + + +func test_is_null() -> void: + assert_dict(null).is_null() + + assert_failure(func() -> void: assert_dict({}).is_null()) \ + .is_failed() \ + .has_message("Expecting: '' but was '{ }'") + + +func test_is_not_null() -> void: + assert_dict({}).is_not_null() + + assert_failure(func() -> void: assert_dict(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_dict({}).is_equal({}) + assert_dict({1:1}).is_equal({1:1}) + assert_dict({1:1, "key_a": "value_a"}).is_equal({1:1, "key_a": "value_a" }) + # different order is also equals + assert_dict({"key_a": "value_a", 1:1}).is_equal({1:1, "key_a": "value_a" }) + + # should fail + assert_failure(func() -> void: assert_dict(null).is_equal({1:1})) \ + .is_failed() \ + .has_message(""" + Expecting: + '{ + 1: 1 + }' + but was + ''""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict({}).is_equal({1:1})).is_failed() + assert_failure(func() -> void: assert_dict({1:1}).is_equal({})).is_failed() + assert_failure(func() -> void: assert_dict({1:1}).is_equal({1:2})).is_failed() + assert_failure(func() -> void: assert_dict({1:2}).is_equal({1:1})).is_failed() + assert_failure(func() -> void: assert_dict({1:1}).is_equal({1:1, "key_a": "value_a"})).is_failed() + assert_failure(func() -> void: assert_dict({1:1, "key_a": "value_a"}).is_equal({1:1})).is_failed() + assert_failure(func() -> void: assert_dict({1:1, "key_a": "value_a"}).is_equal({1:1, "key_b": "value_b"})).is_failed() + assert_failure(func() -> void: assert_dict({1:1, "key_b": "value_b"}).is_equal({1:1, "key_a": "value_a"})).is_failed() + assert_failure(func() -> void: assert_dict({"key_a": "value_a", 1:1}).is_equal({1:1, "key_b": "value_b"})).is_failed() + assert_failure(func() -> void: assert_dict({1:1, "key_b": "value_b"}).is_equal({"key_a": "value_a", 1:1})) \ + .is_failed() \ + .has_message(""" + Expecting: + '{ + 1: 1, + "key_a": "value_a" + }' + but was + '{ + 1: 1, + "key_b": "value_b" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_not_equal() -> void: + assert_dict(null).is_not_equal({}) + assert_dict({}).is_not_equal(null) + assert_dict({}).is_not_equal({1:1}) + assert_dict({1:1}).is_not_equal({}) + assert_dict({1:1}).is_not_equal({1:2}) + assert_dict({2:1}).is_not_equal({1:1}) + assert_dict({1:1}).is_not_equal({1:1, "key_a": "value_a"}) + assert_dict({1:1, "key_a": "value_a"}).is_not_equal({1:1}) + assert_dict({1:1, "key_a": "value_a"}).is_not_equal({1:1, "key_b": "value_b"}) + + # should fail + assert_failure(func() -> void: assert_dict({}).is_not_equal({})).is_failed() + assert_failure(func() -> void: assert_dict({1:1}).is_not_equal({1:1})).is_failed() + assert_failure(func() -> void: assert_dict({1:1, "key_a": "value_a"}).is_not_equal({1:1, "key_a": "value_a"})).is_failed() + assert_failure(func() -> void: assert_dict({"key_a": "value_a", 1:1}).is_not_equal({1:1, "key_a": "value_a"})) \ + .is_failed() \ + .has_message(""" + Expecting: + '{ + 1: 1, + "key_a": "value_a" + }' + not equal to + '{ + 1: 1, + "key_a": "value_a" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_same() -> void: + var dict_a := {} + var dict_b := {"key"="value", "key2"="value"} + var dict_c := {1:1, "key_a": "value_a"} + var dict_d := {"key_a": "value_a", 1:1} + assert_dict(dict_a).is_same(dict_a) + assert_dict(dict_b).is_same(dict_b) + assert_dict(dict_c).is_same(dict_c) + assert_dict(dict_d).is_same(dict_d) + + assert_failure(func() -> void: assert_dict({}).is_same({})) \ + .is_failed()\ + .has_message(""" + Expecting: + '{ }' + to refer to the same object + '{ }'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict({1:1, "key_a": "value_a"}).is_same({1:1, "key_a": "value_a" })) \ + .is_failed()\ + .has_message(""" + Expecting: + '{ + 1: 1, + "key_a": "value_a" + }' + to refer to the same object + '{ + 1: 1, + "key_a": "value_a" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_not_same() -> void: + var dict_a := {} + var dict_b := {} + var dict_c := {1:1, "key_a": "value_a"} + var dict_d := {1:1, "key_a": "value_a"} + assert_dict(dict_a).is_not_same(dict_b).is_not_same(dict_c).is_not_same(dict_d) + assert_dict(dict_b).is_not_same(dict_a).is_not_same(dict_c).is_not_same(dict_d) + assert_dict(dict_c).is_not_same(dict_a).is_not_same(dict_b).is_not_same(dict_d) + assert_dict(dict_d).is_not_same(dict_a).is_not_same(dict_b).is_not_same(dict_c) + + assert_failure(func() -> void: assert_dict(dict_a).is_not_same(dict_a)) \ + .is_failed()\ + .has_message(""" + Expecting not same: + '{ }'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict(dict_c).is_not_same(dict_c)) \ + .is_failed()\ + .has_message(""" + Expecting not same: + '{ + 1: 1, + "key_a": "value_a" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_empty() -> void: + assert_dict({}).is_empty() + + assert_failure(func() -> void: assert_dict(null).is_empty()) \ + .is_failed() \ + .has_message("Expecting:\n" + + " must be empty but was\n" + + " ''") + assert_failure(func() -> void: assert_dict({1:1}).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + '{ + 1: 1 + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_not_empty() -> void: + assert_dict({1:1}).is_not_empty() + assert_dict({1:1, "key_a": "value_a"}).is_not_empty() + + assert_failure(func() -> void: assert_dict(null).is_not_empty()) \ + .is_failed() \ + .has_message("Expecting:\n" + + " must not be empty") + assert_failure(func() -> void: assert_dict({}).is_not_empty()).is_failed() + + +func test_has_size() -> void: + assert_dict({}).has_size(0) + assert_dict({1:1}).has_size(1) + assert_dict({1:1, 2:1}).has_size(2) + assert_dict({1:1, 2:1, 3:1}).has_size(3) + + assert_failure(func() -> void: assert_dict(null).has_size(0))\ + .is_failed() \ + .has_message("Expecting: not to be ''") + assert_failure(func() -> void: assert_dict(null).has_size(1)).is_failed() + assert_failure(func() -> void: assert_dict({}).has_size(1)).is_failed() + assert_failure(func() -> void: assert_dict({1:1}).has_size(0)).is_failed() + assert_failure(func() -> void: assert_dict({1:1}).has_size(2)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '2' + but was + '1'""" + .dedent() + .trim_prefix("\n") + ) + +class TestObj: + var _name :String + var _value :int + + func _init(name :String = "Foo", value :int = 0) -> void: + _name = name + _value = value + + func _to_string() -> String: + return "class:%s:%d" % [_name, _value] + + +func test_contains_keys() -> void: + var key_a := TestObj.new() + var key_b := TestObj.new() + var key_c := TestObj.new() + var key_d := TestObj.new("D") + + assert_dict({1:1, 2:2, 3:3}).contains_keys([2]) + assert_dict({1:1, 2:2, "key_a": "value_a"}).contains_keys([2, "key_a"]) + assert_dict({key_a:1, key_b:2, key_c:3}).contains_keys([key_a, key_b]) + assert_dict({key_a:1, key_c:3 }).contains_keys([key_b]) + assert_dict({key_a:1, 3:3}).contains_keys([key_a, key_b]) + + + assert_failure(func() -> void: assert_dict({1:1, 3:3}).contains_keys([2])) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[1, 3]' + to contains: + '[2]' + but can't find key's: + '[2]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict({1:1, 3:3}).contains_keys([1, 4])) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[1, 3]' + to contains: + '[1, 4]' + but can't find key's: + '[4]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict(null).contains_keys([1, 4])) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + assert_failure(func() -> void: assert_dict({key_a:1, 3:3}).contains_keys([key_a, key_d])) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[class:Foo:0, 3]' + to contains: + '[class:Foo:0, class:D:0]' + but can't find key's: + '[class:D:0]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_keys_variadic_args() -> void: + var key_a := TestObj.new() + var key_b := TestObj.new() + var key_c := TestObj.new() + var key_d := TestObj.new("D") + + assert_dict({1:1, 2:2, 3:3}).contains_keys(2) + assert_dict({1:1, 2:2, "key_a": "value_a"}).contains_keys(2, "key_a") + assert_dict({key_a:1, key_b:2, key_c:3}).contains_keys(key_a, key_b) + assert_dict({key_a:1, key_c:3 }).contains_keys(key_b) + assert_dict({key_a:1, 3:3}).contains_keys(key_a, key_b) + + assert_failure(func() -> void: assert_dict({1:1, 3:3}).contains_keys(2)) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[1, 3]' + to contains: + '[2]' + but can't find key's: + '[2]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict({1:1, 3:3}).contains_keys(1, 4)) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[1, 3]' + to contains: + '[1, 4]' + but can't find key's: + '[4]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict(null).contains_keys(1, 4)) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + assert_failure(func() -> void: assert_dict({key_a:1, 3:3}).contains_keys(key_a, key_d)) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[class:Foo:0, 3]' + to contains: + '[class:Foo:0, class:D:0]' + but can't find key's: + '[class:D:0]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_key_value() -> void: + assert_dict({1:1}).contains_key_value(1, 1) + assert_dict({1:1, 2:2, 3:3}).contains_key_value(3, 3).contains_key_value(1, 1) + + assert_failure(func() -> void: assert_dict({1:1}).contains_key_value(1, 2)) \ + .is_failed() \ + .has_message(""" + Expecting contains key and value: + '1' : '2' + but contains + '1' : '1'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict(null).contains_key_value(1, 2)) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_not_contains_keys() -> void: + assert_dict({}).not_contains_keys([2]) + assert_dict({1:1, 3:3}).not_contains_keys([2]) + assert_dict({1:1, 3:3}).not_contains_keys([2, 4]) + + assert_failure(func() -> void: assert_dict({1:1, 2:2, 3:3}).not_contains_keys([2, 4])) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains keys: + '[1, 2, 3]' + do not contains: + '[2, 4]' + but contains key's: + '[2]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict({1:1, 2:2, 3:3}).not_contains_keys([1, 2, 3, 4])) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains keys: + '[1, 2, 3]' + do not contains: + '[1, 2, 3, 4]' + but contains key's: + '[1, 2, 3]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict(null).not_contains_keys([1, 4])) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_not_contains_keys_variadic_args() -> void: + assert_dict({}).not_contains_keys(2) + assert_dict({1:1, 3:3}).not_contains_keys(2) + assert_dict({1:1, 3:3}).not_contains_keys(2, 4) + + assert_failure(func() -> void: assert_dict({1:1, 2:2, 3:3}).not_contains_keys(2, 4)) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains keys: + '[1, 2, 3]' + do not contains: + '[2, 4]' + but contains key's: + '[2]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict({1:1, 2:2, 3:3}).not_contains_keys(1, 2, 3, 4)) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains keys: + '[1, 2, 3]' + do not contains: + '[1, 2, 3, 4]' + but contains key's: + '[1, 2, 3]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict(null).not_contains_keys(1, 4)) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_contains_same_keys() -> void: + var key_a := TestObj.new() + var key_b := TestObj.new() + var key_c := TestObj.new() + + assert_dict({1:1, 2:2, 3:3}).contains_same_keys([2]) + assert_dict({1:1, 2:2, "key_a": "value_a"}).contains_same_keys([2, "key_a"]) + assert_dict({key_a:1, key_b:2, 3:3}).contains_same_keys([key_b]) + assert_dict({key_a:1, key_b:2, 3:3}).contains_same_keys([key_a, key_b]) + + assert_failure(func() -> void: assert_dict({key_a:1, key_c:3 }).contains_same_keys([key_a, key_b])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME keys: + '[class:Foo:0, class:Foo:0]' + to contains: + '[class:Foo:0, class:Foo:0]' + but can't find key's: + '[class:Foo:0]'""" + .dedent().trim_prefix("\n") + ) + + +func test_contains_same_key_value() -> void: + var key_a := TestObj.new("A") + var key_b := TestObj.new("B") + var key_c := TestObj.new("C") + var key_d := TestObj.new("A") + + assert_dict({key_a:1, key_b:2, key_c:3})\ + .contains_same_key_value(key_a, 1)\ + .contains_same_key_value(key_b, 2) + + assert_failure(func() -> void: assert_dict({key_a:1, key_b:2, key_c:3}).contains_same_key_value(key_a, 2)) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME key and value: + : '2' + but contains + : '1'""" + .dedent().trim_prefix("\n") + ) + assert_failure(func() -> void: assert_dict({key_a:1, key_b:2, key_c:3}).contains_same_key_value(key_d, 1)) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME keys: + '[class:A:0, class:B:0, class:C:0]' + to contains: + '[class:A:0]' + but can't find key's: + '[class:A:0]'""" + .dedent().trim_prefix("\n") + ) + + +func test_not_contains_same_keys() -> void: + var key_a := TestObj.new("A") + var key_b := TestObj.new("B") + var key_c := TestObj.new("C") + var key_d := TestObj.new("A") + + assert_dict({}).not_contains_same_keys([key_a]) + assert_dict({key_a:1, key_b:2}).not_contains_same_keys([key_c, key_d]) + + assert_failure(func() -> void: assert_dict({key_a:1, key_b:2}).not_contains_same_keys([key_c, key_b])) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains SAME keys + '[class:A:0, class:B:0]' + do not contains: + '[class:C:0, class:B:0]' + but contains key's: + '[class:B:0]'""" + .dedent().trim_prefix("\n") + ) + + +func test_not_contains_same_keys_variadic_args() -> void: + var key_a := TestObj.new("A") + var key_b := TestObj.new("B") + var key_c := TestObj.new("C") + var key_d := TestObj.new("A") + + assert_dict({}).not_contains_same_keys(key_a) + assert_dict({key_a:1, key_b:2}).not_contains_same_keys(key_c, key_d) + + assert_failure(func() -> void: assert_dict({key_a:1, key_b:2}).not_contains_same_keys(key_c, key_b)) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains SAME keys + '[class:A:0, class:B:0]' + do not contains: + '[class:C:0, class:B:0]' + but contains key's: + '[class:B:0]'""" + .dedent().trim_prefix("\n") + ) + + +func test_override_failure_message() -> void: + assert_object(assert_dict({1:1}).override_failure_message("error")).is_instanceof(GdUnitDictionaryAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_dict({1:1}) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_dict({1:1}).append_failure_message("error")).is_instanceof(GdUnitDictionaryAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_dict({1:1}) \ + .append_failure_message("custom failure data") \ + .is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + '{ + 1: 1 + }' + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_dict({}).is_empty() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_dict({}).is_not_empty()).is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_dict({}).is_empty() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd.uid new file mode 100644 index 0000000..8664125 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://c2mcnbilexlat diff --git a/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd new file mode 100644 index 0000000..aa2daac --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd @@ -0,0 +1,117 @@ +# GdUnit generated TestSuite +class_name GdUnitFailureAssertImplTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd' + + +func last_assert() -> Variant: + return GdUnitThreadManager.get_current_context().get_assert() + + +func test_has_line() -> void: + assert_failure(func() -> void: assert_bool(true).is_false()) \ + .is_failed() \ + .has_line(16) + + +func test_has_message() -> void: + assert_failure(func() -> void: assert_bool(true).is_true()) \ + .is_success() + assert_failure(func() -> void: assert_bool(true).is_false()) \ + .is_failed()\ + .has_message("Expecting: 'false' but is 'true'") + + +func test_starts_with_message() -> void: + assert_failure(func() -> void: assert_bool(true).is_false()) \ + .is_failed()\ + .starts_with_message("Expecting: 'false' bu") + + +func test_assert_failure_on_invalid_cb() -> void: + assert_failure(func() -> void: prints())\ + .is_failed()\ + .has_message("Invalid Callable! It must be a callable of 'GdUnitAssert'") + + +@warning_ignore("unused_parameter") +func test_assert_failure_on_assert(test_name :String, assert_type :Object, value :Variant, test_parameters := [ + ["GdUnitBoolAssert", GdUnitBoolAssert, true], + ["GdUnitStringAssert", GdUnitStringAssert, "value"], + ["GdUnitIntAssert", GdUnitIntAssert, 42], + ["GdUnitFloatAssert", GdUnitFloatAssert, 42.0], + ["GdUnitObjectAssert", GdUnitObjectAssert, RefCounted.new()], + ["GdUnitVectorAssert", GdUnitVectorAssert, Vector2.ZERO], + ["GdUnitVectorAssert", GdUnitVectorAssert, Vector3.ZERO], + ["GdUnitArrayAssert", GdUnitArrayAssert, Array()], + ["GdUnitDictionaryAssert", GdUnitDictionaryAssert, {}], +]) -> void: + var instance := assert_failure(func() -> void: assert_that(value)) + assert_object(last_assert()).is_instanceof(assert_type) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_file() -> void: + var instance := assert_failure(func() -> void: assert_file("res://foo.gd")) + assert_object(last_assert()).is_instanceof(GdUnitFileAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_func() -> void: + var instance := assert_failure(func() -> void: assert_func(RefCounted.new(), "_to_string")) + assert_object(last_assert()).is_instanceof(GdUnitFuncAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_signal() -> void: + var instance := assert_failure(func() -> void: assert_signal(null)) + assert_object(last_assert()).is_instanceof(GdUnitSignalAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_result() -> void: + var instance := assert_failure(func() -> void: assert_result(null)) + assert_object(last_assert()).is_instanceof(GdUnitResultAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +#region has_stack_trace + +func _stack_fail_1() -> void: + assert_bool(true).is_false() + + +func _stack_fail_2() -> void: + _stack_fail_1() + + +func _stack_fail_3() -> void: + _stack_fail_2() + + +func test_has_stack_trace_depth_1() -> void: + assert_failure(_stack_fail_1) \ + .is_failed() \ + .has_message("Expecting: 'false' but is 'true'") \ + .has_stack_trace([ + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd", 85, "_stack_fail_1"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd", 97, "test_has_stack_trace_depth_1"), + ]) + + +func test_has_stack_trace_depth_3() -> void: + assert_failure(_stack_fail_3) \ + .is_failed() \ + .has_message("Expecting: 'false' but is 'true'") \ + .has_stack_trace([ + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd", 85, "_stack_fail_1"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd", 89, "_stack_fail_2"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd", 93, "_stack_fail_3"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd", 107, "test_has_stack_trace_depth_3"), + ]) + +#endregion diff --git a/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd.uid new file mode 100644 index 0000000..3346f24 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://tqlahybp7e7d diff --git a/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd new file mode 100644 index 0000000..3dd833a --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd @@ -0,0 +1,262 @@ +# GdUnit generated TestSuite +class_name GdUnitFloatAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd' + + +func test_is_null() -> void: + assert_float(null).is_null() + + assert_failure(func() -> void: assert_float(23.2).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was '23.200000'") + + +func test_is_not_null() -> void: + assert_float(23.2).is_not_null() + + assert_failure(func() -> void: assert_float(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_float(23.2).is_equal(23.2) + + assert_failure(func() -> void: assert_float(23.2).is_equal(23.4)) \ + .is_failed() \ + .has_message("Expecting:\n '23.400000'\n but was\n '23.200000'") + assert_failure(func() -> void: assert_float(null).is_equal(23.4)) \ + .is_failed() \ + .has_message("Expecting:\n '23.400000'\n but was\n ''") + + +func test_is_not_equal() -> void: + assert_float(null).is_not_equal(23.4) + assert_float(23.2).is_not_equal(23.4) + + assert_failure(func() -> void: assert_float(23.2).is_not_equal(23.2)) \ + .is_failed() \ + .has_message("Expecting:\n '23.200000'\n not equal to\n '23.200000'") + + +func test_is_equal_approx() -> void: + assert_float(23.2).is_equal_approx(23.2, 0.01) + assert_float(23.19).is_equal_approx(23.2, 0.01) + assert_float(23.20).is_equal_approx(23.2, 0.01) + assert_float(23.21).is_equal_approx(23.2, 0.01) + + assert_failure(func() -> void: assert_float(23.18).is_equal_approx(23.2, 0.01)) \ + .is_failed() \ + .has_message("Expecting:\n '23.180000'\n in range between\n '23.190000' <> '23.210000'") + assert_failure(func() -> void: assert_float(23.22).is_equal_approx(23.2, 0.01)) \ + .is_failed() \ + .has_message("Expecting:\n '23.220000'\n in range between\n '23.190000' <> '23.210000'") + assert_failure(func() -> void: assert_float(null).is_equal_approx(23.2, 0.01)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '23.190000' <> '23.210000'") + + + +func test_is_less_() -> void: + assert_failure(func() -> void: assert_float(23.2).is_less(23.2)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23.200000' but was '23.200000'") + +func test_is_less() -> void: + assert_float(23.2).is_less(23.4) + assert_float(23.2).is_less(26.0) + + assert_failure(func() -> void: assert_float(23.2).is_less(23.2)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23.200000' but was '23.200000'") + assert_failure(func() -> void: assert_float(null).is_less(23.2)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23.200000' but was ''") + + +func test_is_less_equal() -> void: + assert_float(23.2).is_less_equal(23.4) + assert_float(23.2).is_less_equal(23.2) + + assert_failure(func() -> void: assert_float(23.2).is_less_equal(23.1)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '23.100000' but was '23.200000'") + assert_failure(func() -> void: assert_float(null).is_less_equal(23.1)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '23.100000' but was ''") + + +func test_is_greater() -> void: + assert_float(23.2).is_greater(23.0) + assert_float(23.4).is_greater(22.1) + + assert_failure(func() -> void: assert_float(23.2).is_greater(23.2)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23.200000' but was '23.200000'") + assert_failure(func() -> void: assert_float(null).is_greater(23.2)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23.200000' but was ''") + + +func test_is_greater_equal() -> void: + assert_float(23.2).is_greater_equal(20.2) + assert_float(23.2).is_greater_equal(23.2) + + assert_failure(func() -> void: assert_float(23.2).is_greater_equal(23.3)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '23.300000' but was '23.200000'") + assert_failure(func() -> void: assert_float(null).is_greater_equal(23.3)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '23.300000' but was ''") + + +func test_is_negative() -> void: + assert_float(-13.2).is_negative() + + assert_failure(func() -> void: assert_float(13.2).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '13.200000' be negative") + assert_failure(func() -> void: assert_float(null).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be negative") + + +func test_is_not_negative() -> void: + assert_float(13.2).is_not_negative() + + assert_failure(func() -> void: assert_float(-13.2).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '-13.200000' be not negative") + assert_failure(func() -> void: assert_float(null).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be not negative") + + +func test_is_zero() -> void: + assert_float(0.0).is_zero() + + assert_failure(func() -> void: assert_float(0.00001).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is '0.000010'") + assert_failure(func() -> void: assert_float(null).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is ''") + + +func test_is_not_zero() -> void: + assert_float(0.00001).is_not_zero() + + assert_failure(func() -> void: assert_float(0.000001).is_not_zero()) \ + .is_failed() \ + .has_message("Expecting:\n not equal to 0") + assert_failure(func() -> void: assert_float(null).is_not_zero()) \ + .is_failed() \ + .has_message("Expecting:\n not equal to 0") + + +func test_is_in() -> void: + assert_float(5.2).is_in([5.1, 5.2, 5.3, 5.4]) + # this assertion fail because 5.5 is not in [5.1, 5.2, 5.3, 5.4] + assert_failure(func() -> void: assert_float(5.5).is_in([5.1, 5.2, 5.3, 5.4])) \ + .is_failed() \ + .has_message("Expecting:\n '5.500000'\n is in\n '[5.1, 5.2, 5.3, 5.4]'") + assert_failure(func() -> void: assert_float(null).is_in([5.1, 5.2, 5.3, 5.4])) \ + .is_failed() \ + .has_message("Expecting:\n ''\n is in\n '[5.1, 5.2, 5.3, 5.4]'") + + +func test_is_not_in() -> void: + assert_float(null).is_not_in([5.1, 5.3, 5.4]) + assert_float(5.2).is_not_in([5.1, 5.3, 5.4]) + # this assertion fail because 5.2 is not in [5.1, 5.2, 5.3, 5.4] + assert_failure(func() -> void: assert_float(5.2).is_not_in([5.1, 5.2, 5.3, 5.4])) \ + .is_failed() \ + .has_message("Expecting:\n '5.200000'\n is not in\n '[5.1, 5.2, 5.3, 5.4]'") + + +func test_is_between() -> void: + assert_float(-20.0).is_between(-20.0, 20.9) + assert_float(10.0).is_between(-20.0, 20.9) + assert_float(20.9).is_between(-20.0, 20.9) + + +func test_is_between_must_fail() -> void: + assert_failure(func() -> void: assert_float(-10.0).is_between(-9.0, 0.0)) \ + .is_failed() \ + .has_message("Expecting:\n '-10.000000'\n in range between\n '-9.000000' <> '0.000000'") + assert_failure(func() -> void: assert_float(0.0).is_between(1, 10)) \ + .is_failed() \ + .has_message("Expecting:\n '0.000000'\n in range between\n '1.000000' <> '10.000000'") + assert_failure(func() -> void: assert_float(10.0).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n '10.000000'\n in range between\n '11.000000' <> '21.000000'") + assert_failure(func() -> void: assert_float(null).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '11.000000' <> '21.000000'") + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func() -> void: assert_float(1)) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_float(true)) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_float("foo")) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_float(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_object(assert_float(3.14).override_failure_message("error")).is_instanceof(GdUnitFloatAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_float(3.14) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_float(3.14).append_failure_message("error")).is_instanceof(GdUnitFloatAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_float(3.14) \ + .append_failure_message("custom failure data") \ + .is_zero()) \ + .is_failed() \ + .has_message(""" + Expecting: + equal to 0 but is '3.140000' + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_float(0.0).is_zero() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_float(1.0).is_zero()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_float(0.0).is_zero() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd.uid new file mode 100644 index 0000000..ae69957 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://vpfsto0y7w7y diff --git a/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd new file mode 100644 index 0000000..a60fa0e --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd @@ -0,0 +1,359 @@ +# GdUnit generated TestSuite +class_name GdUnitFuncAssertImplTest +extends GdUnitTestSuite +@warning_ignore_start("redundant_await") + + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd' +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +class TestValueProvider: + var _max_iterations :int + var _current_itteration := 0 + + func _init(iterations := 0) -> void: + _max_iterations = iterations + + func bool_value() -> bool: + _current_itteration += 1 + if _current_itteration == _max_iterations: + return true + return false + + func int_value() -> int: + return 0 + + func float_value() -> float: + return 0.0 + + func string_value() -> String: + return "value" + + func object_value() -> Object: + return Resource.new() + + func array_value() -> Array: + return [] + + func dict_value() -> Dictionary: + return {} + + func vec2_value() -> Vector2: + return Vector2.ONE + + func vec3_value() -> Vector3: + return Vector3.ONE + + func no_value() -> void: + pass + + func unknown_value() -> Vector3: + return Vector3.ONE + + +class ValueProvidersWithArguments: + + func is_type(_type :int) -> bool: + return true + + func get_index(_instance :Object, _name :String) -> int: + return 1 + + func get_index2(_instance :Object, _name :String, _recursive := false) -> int: + return 1 + + +class TestIterativeValueProvider: + var _max_iterations :int + var _current_itteration := 0 + var _inital_value :Variant + var _final_value :Variant + + func _init(inital_value :Variant, iterations :int, final_value :Variant) -> void: + _max_iterations = iterations + _inital_value = inital_value + _final_value = final_value + + func bool_value() -> bool: + _current_itteration += 1 + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func int_value() -> int: + _current_itteration += 1 + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func obj_value() -> Variant: + _current_itteration += 1 + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func has_type(type :int, _recursive :bool = true) -> int: + _current_itteration += 1 + #await (Engine.get_main_loop() as SceneTree).idle_frame + if type == _current_itteration: + return _final_value + return _inital_value + + func await_value() -> int: + _current_itteration += 1 + await (Engine.get_main_loop() as SceneTree).process_frame + prints("yielded_value", _current_itteration) + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func reset() -> void: + _current_itteration = 0 + + func iteration() -> int: + return _current_itteration + + +@warning_ignore("unused_parameter") +func test_is_null(timeout := 2000) -> void: + var value_provider := TestIterativeValueProvider.new(RefCounted.new(), 5, null) + # without default timeout od 2000ms + assert_func(value_provider, "obj_value").is_not_null() + await assert_func(value_provider, "obj_value").is_null() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "obj_value").is_not_null() + await assert_func(value_provider, "obj_value").wait_until(5000).is_null() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + value_provider = TestIterativeValueProvider.new(RefCounted.new(), 1, RefCounted.new()) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "obj_value", []).wait_until(100).is_null()) + ).has_message("Expected: is null but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_not_null(timeout := 2000) -> void: + var value_provider := TestIterativeValueProvider.new(null, 5, RefCounted.new()) + # without default timeout od 2000ms + assert_func(value_provider, "obj_value").is_null() + await assert_func(value_provider, "obj_value").is_not_null() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "obj_value").is_null() + await assert_func(value_provider, "obj_value").wait_until(5000).is_not_null() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + value_provider = TestIterativeValueProvider.new(null, 1, null) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "obj_value", []).wait_until(100).is_not_null()) + ).has_message("Expected: is not null but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_true(timeout := 2000) -> void: + var value_provider := TestIterativeValueProvider.new(false, 5, true) + # without default timeout od 2000ms + assert_func(value_provider, "bool_value").is_false() + await assert_func(value_provider, "bool_value").is_true() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "bool_value").is_false() + await assert_func(value_provider, "bool_value").wait_until(5000).is_true() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + value_provider = TestIterativeValueProvider.new(false, 1, false) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "bool_value", []).wait_until(100).is_true()) + ).has_message("Expected: is true but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_false(timeout := 2000) -> void: + var value_provider := TestIterativeValueProvider.new(true, 5, false) + # without default timeout od 2000ms + assert_func(value_provider, "bool_value").is_true() + await assert_func(value_provider, "bool_value").is_false() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "bool_value").is_true() + await assert_func(value_provider, "bool_value").wait_until(5000).is_false() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + value_provider = TestIterativeValueProvider.new(true, 1, true) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "bool_value", []).wait_until(100).is_false()) + ).has_message("Expected: is false but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_equal(timeout := 2000) -> void: + var value_provider := TestIterativeValueProvider.new(42, 5, 23) + # without default timeout od 2000ms + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").is_equal(23) + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").wait_until(5000).is_equal(23) + assert_int(value_provider.iteration()).is_equal(5) + + # failing case + value_provider = TestIterativeValueProvider.new(23, 1, 23) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "int_value", []).wait_until(100).is_equal(25)) + ).has_message("Expected: is equal '25' but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_not_equal(timeout := 2000) -> void: + var value_provider := TestIterativeValueProvider.new(42, 5, 23) + # without default timeout od 2000ms + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").is_not_equal(42) + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").wait_until(5000).is_not_equal(42) + assert_int(value_provider.iteration()).is_equal(5) + + # failing case + value_provider = TestIterativeValueProvider.new(23, 1, 23) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "int_value", []).wait_until(100).is_not_equal(23)) + ).has_message("Expected: is not equal '23' but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_equal_wiht_func_arg(timeout := 1300) -> void: + var value_provider := TestIterativeValueProvider.new(42, 10, 23) + # without default timeout od 2000ms + assert_func(value_provider, "has_type", [1]).is_equal(42) + await assert_func(value_provider, "has_type", [10]).is_equal(23) + assert_int(value_provider.iteration()).is_equal(10) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "has_type", [1]).is_equal(42) + await assert_func(value_provider, "has_type", [10]).wait_until(5000).is_equal(23) + assert_int(value_provider.iteration()).is_equal(10) + + +# abort test after 500ms to fail +@warning_ignore("unused_parameter") +func test_timeout_and_assert_fails(timeout := 500) -> void: + # disable temporary the timeout errors for this test + discard_error_interupted_by_timeout() + var value_provider := TestIterativeValueProvider.new(1, 10, 10) + # wait longer than test timeout, the value will be never '42' + await assert_func(value_provider, "int_value").wait_until(1000).is_equal(42) + fail("The test must be interrupted after 500ms") + + +func timed_function() -> Color: + var color := Color.RED + await await_millis(20) + color = Color.GREEN + await await_millis(20) + color = Color.BLUE + await await_millis(20) + color = Color.BLACK + return color + + +func test_timer_yielded_function() -> void: + await assert_func(self, "timed_function").is_equal(Color.BLACK) + # will be never red + await assert_func(self, "timed_function").wait_until(100).is_not_equal(Color.RED) + # failure case + ( + await assert_failure_await(func() -> void: await assert_func(self, "timed_function", []).wait_until(100).is_equal(Color.RED)) + ).has_message("Expected: is equal 'Color$v0' but timed out after 100ms" + .replace("$v0", str(Color.RED)) + ) + + +func test_timer_yielded_function_timeout() -> void: + ( + await assert_failure_await(func() -> void: await assert_func(self, "timed_function", []).wait_until(40).is_equal(Color.BLACK)) + ).has_message("Expected: is equal 'Color()' but timed out after 40ms") + + +func yielded_function() -> Color: + var color := Color.RED + await get_tree().process_frame + color = Color.GREEN + await get_tree().process_frame + color = Color.BLUE + await get_tree().process_frame + color = Color.BLACK + return color + + +func test_idle_frame_yielded_function() -> void: + await assert_func(self, "yielded_function").is_equal(Color.BLACK) + ( + await assert_failure_await(func() -> void: await assert_func(self, "yielded_function", []).wait_until(500).is_equal(Color.RED)) + ).has_message("Expected: is equal 'Color$v0' but timed out after 500ms" + .replace("$v0", str(Color.RED)) + ) + + +func test_has_failure_message() -> void: + var value_provider := TestIterativeValueProvider.new(10, 1, 10) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "int_value", []).wait_until(500).is_equal(42)) + ).has_message("Expected: is equal '42' but timed out after 500ms") + + +func test_override_failure_message() -> void: + assert_object(assert_func(RefCounted.new(), "get_reference_count").override_failure_message("error")).is_instanceof(GdUnitFuncAssert) + var value_provider := TestIterativeValueProvider.new(10, 1, 20) + ( + await assert_failure_await(func() -> void: await assert_func(value_provider, "int_value", []) \ + .override_failure_message("Custom failure message") \ + .wait_until(100) \ + .is_equal(42)) + ).has_message("Custom failure message") + + +@warning_ignore("unsafe_method_access") +func test_append_failure_message() -> void: + assert_object(assert_func(RefCounted.new(), "get_reference_count").append_failure_message("error")).is_instanceof(GdUnitFuncAssert) + ( + await assert_failure_await(func() -> void: await assert_func(RefCounted.new(), "get_reference_count") \ + .append_failure_message("custom failure data") \ + .wait_until(10)\ + .is_equal(42)) + ).is_failed() \ + .contains_message("Expected: is equal '42' but timed out after") \ + .contains_message(""" + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +@warning_ignore("unused_parameter") +func test_invalid_function(timeout := 100) -> void: + ( + await assert_failure_await(func() -> void: await assert_func(self, "invalid_func_name", [])\ + .wait_until(1000)\ + .is_equal(42)) + ).starts_with_message("The function 'invalid_func_name' do not exists checked instance") diff --git a/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd.uid new file mode 100644 index 0000000..97354a5 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://dpc8qjoayca7m diff --git a/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd new file mode 100644 index 0000000..41d69e0 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd @@ -0,0 +1,160 @@ +# GdUnit generated TestSuite +class_name GdUnitGodotErrorAssertImplTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd' + +# skip see https://github.com/godotengine/godot/issues/80292 +func before() -> void: + # disable default error reporting for testing + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, false) + + +func after_test() -> void: + # Cleanup report artifacts + GdUnitThreadManager.get_current_context().get_execution_context().error_monitor.clear_logs() + + +func test_invalid_callable() -> void: + assert_failure(func() -> void: assert_error(Callable()).is_success())\ + .is_failed()\ + .has_message("Invalid Callable 'null::null'") + + +func test_is_success() -> void: + assert_error(produce_assert_success).is_success() + + assert_failure(func() -> void: + assert_error(produce_push_warning).is_success() + ).is_failed().has_message(""" + Expecting: no error's are ocured. + but found: 'this is an push_warning' + """.dedent().trim_prefix("\n")) + + assert_failure(func() -> void: + assert_error(produce_push_error).is_success() + ).is_failed().has_message(""" + Expecting: no error's are ocured. + but found: 'this is an push_error' + """.dedent().trim_prefix("\n")) + + +func test_is_assert_failed( + _do_skip := Engine.is_embedded_in_editor() or OS.is_debug_build(), + _skip_reason := "Is skipped because the test will holt on script error in debug mode") -> void: + + assert_error(produce_assert_error)\ + .is_runtime_error('Assertion failed: this is an assert error') + + assert_failure(func() -> void: + assert_error(produce_assert_success).is_runtime_error('Assertion failed: this is an assert error') + ).is_failed().has_message(""" + Expecting: a runtime error is triggered. + expected: 'Assertion failed: this is an assert error' + current: 'no errors' + """.dedent().trim_prefix("\n")) + + +func test_is_push_warning() -> void: + assert_error(produce_push_warning).is_push_warning('this is an push_warning') + + assert_failure(func() -> void: + assert_error(produce_push_warning).is_push_warning('this is an error') + ).is_failed().has_message(""" + Expecting: push_warning() is called. + expected: 'this is an error' + current: 'this is an push_warning' + """.dedent().trim_prefix("\n")) + + +func test_is_push_warning_using_argument_matcher() -> void: + assert_error(produce_push_warning).is_push_warning(any()) + assert_error(produce_push_warning).is_push_warning(any_string()) + + assert_failure(func() -> void: + assert_error(produce_push_warning).is_push_warning(any_int()) + ).is_failed().has_message("Only 'any()' and 'any_string()' argument matchers are allowed!") + + +func test_is_push_error() -> void: + assert_error(produce_push_error).is_push_error('this is an push_error') + + assert_failure(func() -> void: + assert_error(produce_assert_success).is_push_error('this is an push_error') + ).is_failed().has_message(""" + Expecting: push_error() is called. + expected: 'this is an push_error' + current: 'no errors' + """.dedent().trim_prefix("\n")) + + assert_failure(func() -> void: + assert_error(produce_push_warning).is_push_error('this is an push_error') + ).is_failed().has_message(""" + Expecting: push_error() is called. + expected: 'this is an push_error' + current: 'this is an push_warning' + """.dedent().trim_prefix("\n")) + + +func test_is_push_error_using_argument_matcher() -> void: + assert_error(produce_push_error).is_push_error(any()) + assert_error(produce_push_error).is_push_error(any_string()) + + assert_failure(func() -> void: + assert_error(produce_push_error).is_push_error(any_int()) + ).is_failed().has_message("Only 'any()' and 'any_string()' argument matchers are allowed!") + + +func test_is_runtime_error( + _do_skip := Engine.is_embedded_in_editor() or OS.is_debug_build(), + _skip_reason := "Is skipped because the test will holt on script error in debug mode") -> void: + + assert_error(produce_runtime_error).is_runtime_error("Division by zero error in operator '/'.") + + assert_failure(func() -> void: + assert_error(produce_assert_success).is_runtime_error("Division by zero error in operator '/'.") + ).is_failed().has_message(""" + Expecting: a runtime error is triggered. + expected: 'Division by zero error in operator '/'.' + current: 'no errors' + """.dedent().trim_prefix("\n")) + + +func test_is_runtime_error_using_argument_matcher( + _do_skip := Engine.is_embedded_in_editor() or OS.is_debug_build(), + _skip_reason := "Is skipped because the test will holt on script error in debug mode") -> void: + + assert_error(produce_runtime_error).is_runtime_error(any()) + assert_error(produce_runtime_error).is_runtime_error(any_string()) + + assert_failure(func() -> void: + assert_error(produce_runtime_error).is_runtime_error(any_int()) + ).is_failed().has_message("Only 'any()' and 'any_string()' argument matchers are allowed!") + + +func produce_assert_success() -> void: + @warning_ignore("assert_always_true") + assert(true, "no error" ) + + +func produce_assert_error() -> void: + assert(false, "this is an assert error" ) + + +func produce_push_warning() -> void: + push_warning('this is an push_warning') + + +func produce_push_error() -> void: + push_error('this is an push_error') + + +func produce_runtime_error() -> void: + var a := 0 + @warning_ignore("integer_division") + @warning_ignore("unused_variable") + var x := 1/a diff --git a/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd.uid new file mode 100644 index 0000000..f9fb4cf --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://bobx0upvytgsr diff --git a/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd b/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd new file mode 100644 index 0000000..ae2994c --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd @@ -0,0 +1,38 @@ +extends GdUnitTestSuite + + +var _catched_events: Array[GdUnitEvent] = [] + + +func test_assert_method_with_enabled_global_error_report() -> void: + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, true) + assert_error(do_a_fail).is_runtime_error('Assertion failed: test') + + +func test_assert_method_with_disabled_global_error_report() -> void: + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, false) + assert_error(do_a_fail).is_runtime_error('Assertion failed: test') + + +func do_a_fail() -> void: + @warning_ignore("assert_always_false") + assert(3 == 1, 'test') + + +func catch_test_events(event :GdUnitEvent) -> void: + _catched_events.append(event) + + +func before( + _do_skip := Engine.is_embedded_in_editor() or OS.is_debug_build(), + _skip_reason := "Is skipped because the test will holt on script error in debug mode" + ) -> void: + + GdUnitSignals.instance().gdunit_event.connect(catch_test_events) + + +func after() -> void: + # We expect no errors or failures, as we caught already the assert error by using the assert `assert_error` on the test case + assert_array(_catched_events).extractv(extr("error_count"), extr("failed_count"))\ + .contains_exactly([tuple(0, 0), tuple(0,0), tuple(0,0), tuple(0,0)]) + GdUnitSignals.instance().gdunit_event.disconnect(catch_test_events) diff --git a/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd.uid new file mode 100644 index 0000000..29e7f8c --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd.uid @@ -0,0 +1 @@ +uid://7fves77gvu85 diff --git a/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd new file mode 100644 index 0000000..33aa4b4 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd @@ -0,0 +1,258 @@ +# GdUnit generated TestSuite +class_name GdUnitIntAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd' + + +func test_is_null() -> void: + assert_int(null).is_null() + + assert_failure(func() -> void: assert_int(23).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was '23'") + + +func test_is_not_null() -> void: + assert_int(23).is_not_null() + + assert_failure(func() -> void: assert_int(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_int(23).is_equal(23) + + assert_failure(func() -> void: assert_int(23).is_equal(42)) \ + .is_failed() \ + .has_message("Expecting:\n '42'\n but was\n '23'") + assert_failure(func() -> void: assert_int(null).is_equal(42)) \ + .is_failed() \ + .has_message("Expecting:\n '42'\n but was\n ''") + + +func test_is_not_equal() -> void: + assert_int(null).is_not_equal(42) + assert_int(23).is_not_equal(42) + + assert_failure(func() -> void: assert_int(23).is_not_equal(23)) \ + .is_failed() \ + .has_message("Expecting:\n '23'\n not equal to\n '23'") + + +func test_is_less() -> void: + assert_int(23).is_less(42) + assert_int(23).is_less(24) + + assert_failure(func() -> void: assert_int(23).is_less(23)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23' but was '23'") + assert_failure(func() -> void: assert_int(null).is_less(23)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23' but was ''") + + +func test_is_less_equal() -> void: + assert_int(23).is_less_equal(42) + assert_int(23).is_less_equal(23) + + assert_failure(func() -> void: assert_int(23).is_less_equal(22)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '22' but was '23'") + assert_failure(func() -> void: assert_int(null).is_less_equal(22)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '22' but was ''") + + +func test_is_greater() -> void: + assert_int(23).is_greater(20) + assert_int(23).is_greater(22) + + assert_failure(func() -> void: assert_int(23).is_greater(23)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23' but was '23'") + assert_failure(func() -> void: assert_int(null).is_greater(23)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23' but was ''") + + +func test_is_greater_equal() -> void: + assert_int(23).is_greater_equal(20) + assert_int(23).is_greater_equal(23) + + assert_failure(func() -> void: assert_int(23).is_greater_equal(24)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '24' but was '23'") + assert_failure(func() -> void: assert_int(null).is_greater_equal(24)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '24' but was ''") + + +func test_is_even() -> void: + assert_int(12).is_even() + + assert_failure(func() -> void: assert_int(13).is_even()) \ + .is_failed() \ + .has_message("Expecting:\n '13' must be even") + assert_failure(func() -> void: assert_int(null).is_even()) \ + .is_failed() \ + .has_message("Expecting:\n '' must be even") + + +func test_is_odd() -> void: + assert_int(13).is_odd() + + assert_failure(func() -> void: assert_int(12).is_odd()) \ + .is_failed() \ + .has_message("Expecting:\n '12' must be odd") + assert_failure(func() -> void: assert_int(null).is_odd()) \ + .is_failed() \ + .has_message("Expecting:\n '' must be odd") + + +func test_is_negative() -> void: + assert_int(-13).is_negative() + + assert_failure(func() -> void: assert_int(13).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '13' be negative") + assert_failure(func() -> void: assert_int(null).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be negative") + + +func test_is_not_negative() -> void: + assert_int(13).is_not_negative() + + assert_failure(func() -> void: assert_int(-13).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '-13' be not negative") + assert_failure(func() -> void: assert_int(null).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be not negative") + + +func test_is_zero() -> void: + assert_int(0).is_zero() + + assert_failure(func() -> void: assert_int(1).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is '1'") + assert_failure(func() -> void: assert_int(null).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is ''") + + +func test_is_not_zero() -> void: + assert_int(null).is_not_zero() + assert_int(1).is_not_zero() + + assert_failure(func() -> void: assert_int(0).is_not_zero()) \ + .is_failed() \ + .has_message("Expecting:\n not equal to 0") + + +func test_is_in() -> void: + assert_int(5).is_in([3, 4, 5, 6]) + # this assertion fail because 7 is not in [3, 4, 5, 6] + assert_failure(func() -> void: assert_int(7).is_in([3, 4, 5, 6])) \ + .is_failed() \ + .has_message("Expecting:\n '7'\n is in\n '[3, 4, 5, 6]'") + assert_failure(func() -> void: assert_int(null).is_in([3, 4, 5, 6])) \ + .is_failed() \ + .has_message("Expecting:\n ''\n is in\n '[3, 4, 5, 6]'") + + +func test_is_not_in() -> void: + assert_int(null).is_not_in([3, 4, 6, 7]) + assert_int(5).is_not_in([3, 4, 6, 7]) + # this assertion fail because 7 is not in [3, 4, 5, 6] + assert_failure(func() -> void: assert_int(5).is_not_in([3, 4, 5, 6])) \ + .is_failed() \ + .has_message("Expecting:\n '5'\n is not in\n '[3, 4, 5, 6]'") + + +func test_is_between(fuzzer := Fuzzers.rangei(-20, 20)) -> void: + var value: int = fuzzer.next_value() + assert_int(value).is_between(-20, 20) + + +func test_is_between_must_fail() -> void: + assert_failure(func() -> void: assert_int(-10).is_between(-9, 0)) \ + .is_failed() \ + .has_message("Expecting:\n '-10'\n in range between\n '-9' <> '0'") + assert_failure(func() -> void: assert_int(0).is_between(1, 10)) \ + .is_failed() \ + .has_message("Expecting:\n '0'\n in range between\n '1' <> '10'") + assert_failure(func() -> void: assert_int(10).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n '10'\n in range between\n '11' <> '21'") + assert_failure(func() -> void: assert_int(null).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '11' <> '21'") + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func() -> void: assert_int(3.3)) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_int(true)) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_int("foo")) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_int(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_object(assert_int(314).override_failure_message("error")).is_instanceof(GdUnitIntAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_int(314)\ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_int(314).append_failure_message("error")).is_instanceof(GdUnitIntAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_int(314) \ + .append_failure_message("custom failure data") \ + .is_zero()) \ + .is_failed() \ + .has_message(""" + Expecting: + equal to 0 but is '314' + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_int(0).is_zero() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_int(1).is_zero()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_int(0).is_zero() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd.uid new file mode 100644 index 0000000..0761706 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://2gwaod2sdn6g diff --git a/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd new file mode 100644 index 0000000..6fe9063 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd @@ -0,0 +1,249 @@ +# GdUnit generated TestSuite +class_name GdUnitObjectAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd' + + +func test_is_equal() -> void: + assert_object(Mesh.new()).is_equal(Mesh.new()) + + assert_failure(func() -> void: assert_object(Mesh.new()).is_equal(Skin.new())) \ + .is_failed() + assert_failure(func() -> void: assert_object(null).is_equal(Skin.new())) \ + .is_failed() \ + .has_message("Expecting:\n" + + " \n" + + " but was\n" + + " ''") + + +func test_is_not_equal() -> void: + assert_object(null).is_not_equal(Skin.new()) + assert_object(Mesh.new()).is_not_equal(Skin.new()) + + assert_failure(func() -> void: assert_object(Mesh.new()).is_not_equal(Mesh.new())) \ + .is_failed() + + +func test_is_instanceof() -> void: + # engine class test + assert_object(auto_free(Path3D.new())).is_instanceof(Node) + assert_object(auto_free(Camera3D.new())).is_instanceof(Camera3D) + # script class test + assert_object(auto_free(Udo.new())).is_instanceof(Person) + # inner class test + assert_object(auto_free(CustomClass.InnerClassA.new())).is_instanceof(Node) + assert_object(auto_free(CustomClass.InnerClassB.new())).is_instanceof(CustomClass.InnerClassA) + + assert_failure(func() -> void: assert_object(auto_free(Path3D.new())).is_instanceof(Tree)) \ + .is_failed() \ + .has_message("Expected instance of:\n 'Tree'\n But it was 'Path3D'") + assert_failure(func() -> void: assert_object(null).is_instanceof(Tree)) \ + .is_failed() \ + .has_message("Expected instance of:\n 'Tree'\n But it was ''") + + +func test_is_not_instanceof() -> void: + assert_object(null).is_not_instanceof(Tree) + # engine class test + assert_object(auto_free(Path3D.new())).is_not_instanceof(Tree) + # script class test + assert_object(auto_free(City.new())).is_not_instanceof(Person) + # inner class test + assert_object(auto_free(CustomClass.InnerClassA.new())).is_not_instanceof(Tree) + assert_object(auto_free(CustomClass.InnerClassB.new())).is_not_instanceof(CustomClass.InnerClassC) + + assert_failure(func() -> void: assert_object(auto_free(Path3D.new())).is_not_instanceof(Node)) \ + .is_failed() \ + .has_message("Expected not be a instance of ") + + +func test_is_inheriting() -> void: + # test on native Godot class + assert_object(auto_free(TabContainer.new()))\ + .is_inheriting(Container)\ + .is_inheriting(Control)\ + # we need to specify by string name because is an abstract class + .is_inheriting("CanvasItem")\ + .is_inheriting(Node)\ + .is_inheriting(Object) + assert_failure(func() -> void: + assert_object(auto_free(TabContainer.new())).is_inheriting(Node3D) + ).is_failed().has_message("Expected type to inherit from ") + + # test on user custom class + assert_object(auto_free(MyNode.new()))\ + .is_inheriting(Node)\ + .is_inheriting(Object) + assert_object(auto_free(MyExtendedNode.new()))\ + .is_inheriting(GdUnitObjectAssertImplTest.MyNode)\ + .is_inheriting(Node)\ + .is_inheriting(Object) + assert_failure(func() -> void: + assert_object(auto_free(MyExtendedNode.new())).is_inheriting(Node3D) + ).is_failed().has_message("Expected type to inherit from ") + + # using not Object type + assert_failure(func() -> void: + assert_object([]).is_inheriting(Node) + ).is_failed().has_message("Expected '[]' to inherit from at least Object.") + + +func test_is_not_inheriting() -> void: + # test on native Godot class + assert_object(auto_free(TabContainer.new()))\ + .is_not_inheriting(Node2D)\ + .is_not_inheriting(Node3D) + + assert_failure(func() -> void: + assert_object(auto_free(TabContainer.new()))\ + .is_not_inheriting(Node2D)\ + .is_not_inheriting(Container) + ).is_failed().has_message("Expected type to not inherit from ") + # using not Object type + assert_failure(func() -> void: + assert_object([]).is_not_inheriting(Node) + ).is_failed().has_message("Expected '[]' to inherit from at least Object.") + +func test_is_null() -> void: + assert_object(null).is_null() + + assert_failure(func() -> void: assert_object(auto_free(Node.new())).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was ") + + +func test_is_not_null() -> void: + assert_object(auto_free(Node.new())).is_not_null() + + assert_failure(func() -> void: assert_object(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_same() -> void: + var obj1 :Variant = auto_free(Node.new()) + var obj2 :Variant = obj1 + @warning_ignore("unsafe_method_access") + var obj3 :Variant = auto_free(obj1.duplicate()) + assert_object(obj1).is_same(obj1) + assert_object(obj1).is_same(obj2) + assert_object(obj2).is_same(obj1) + + assert_failure(func() -> void: assert_object(null).is_same(obj1)) \ + .is_failed() \ + .has_message("Expecting:\n" + + " \n" + + " to refer to the same object\n" + + " ''") + assert_failure(func() -> void: assert_object(obj1).is_same(obj3)) \ + .is_failed() + assert_failure(func() -> void: assert_object(obj3).is_same(obj1)) \ + .is_failed() + assert_failure(func() -> void: assert_object(obj3).is_same(obj2)) \ + .is_failed() + + +func test_is_not_same() -> void: + var obj1 :Variant = auto_free(Node.new()) + var obj2 :Variant = obj1 + @warning_ignore("unsafe_method_access") + var obj3 :Variant = auto_free(obj1.duplicate()) + assert_object(null).is_not_same(obj1) + assert_object(obj1).is_not_same(obj3) + assert_object(obj3).is_not_same(obj1) + assert_object(obj3).is_not_same(obj2) + + assert_failure(func() -> void: assert_object(obj1).is_not_same(obj1)) \ + .is_failed() \ + .has_message(""" + Expecting not same: + """ + .dedent() + .trim_prefix("\n")) + assert_failure(func() -> void: assert_object(obj1).is_not_same(obj2)) \ + .is_failed() \ + .has_message(""" + Expecting not same: + """ + .dedent() + .trim_prefix("\n")) + assert_failure(func() -> void: assert_object(obj2).is_not_same(obj1)) \ + .is_failed() \ + .has_message(""" + Expecting not same: + """ + .dedent() + .trim_prefix("\n")) + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func() -> void: assert_object(1)) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_object(1.3)) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_object(true)) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_object("foo")) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_object(assert_object(auto_free(Node.new())).override_failure_message("error")).is_instanceof(GdUnitObjectAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_object(auto_free(Node.new())) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_object(auto_free(Node.new())).append_failure_message("error")).is_instanceof(GdUnitObjectAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_object(auto_free(Node.new())) \ + .append_failure_message("custom failure data") \ + .is_null()) \ + .is_failed() \ + .has_message(""" + Expecting: '' but was + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_object(null).is_null() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_object(RefCounted.new()).is_null()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_object(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() + + +class MyNode extends Node: + pass + +class MyExtendedNode extends MyNode: + pass diff --git a/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd.uid new file mode 100644 index 0000000..e276013 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://cw6s7ooof71x4 diff --git a/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd b/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd new file mode 100644 index 0000000..72b6878 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd @@ -0,0 +1,341 @@ +@warning_ignore_start("unsafe_method_access") +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd' + + +func test_is_array_assert(_test: String, array: Variant, _test_parameters := [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + var assert_ := assert_that(array) + assert_object(assert_).is_instanceof(GdUnitArrayAssert) + + +func test_is_null(_test: String, value: Variant, _test_parameters := [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + assert_array(null).is_null() + assert_failure(func() -> void: assert_array(value).is_null()) \ + .is_failed() \ + .has_message("Expecting: '' but was '%s'" % GdDefaultValueDecoder.decode(value)) + + +func test_is_not_null(_test: String, array: Variant, _test_parameters := [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + assert_array(array).is_not_null() + + assert_failure(func() -> void: assert_array(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal(_test: String, array: Variant, _test_parameters := [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + var other :Variant = array.duplicate() + assert_array(array).is_equal(other) + # should fail because the array not contains same elements and has diff size + other.append(array[2]) + assert_failure(func() -> void: assert_array(array).is_equal(other)) \ + .is_failed() \ + .has_message(""" + Expecting: + '%s' + but was + '%s' + + Differences found: + Index Current Expected 5 $value """ + .dedent() + .trim_prefix("\n") + .replace("$value", str(array[2]) ) % [GdArrayTools.as_string(other, false), GdArrayTools.as_string(array, false)]) + + +func test_is_not_equal(_test: String, array: Variant, _test_parameters := [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + var other :Variant = array.duplicate() + other.append(array[2]) + assert_array(array).is_not_equal(other) + # should fail because the array contains same elements + assert_failure(func() -> void: assert_array(array).is_not_equal(array.duplicate())) \ + .is_failed() \ + .has_message(""" + Expecting: + '%s' + not equal to + '%s'""" + .dedent() + .trim_prefix("\n") % [GdDefaultValueDecoder.decode(array), GdDefaultValueDecoder.decode(array)]) + + +func test_is_empty(_test: String, array: Variant, _test_parameters := [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + var empty :Variant = array.duplicate() + empty.clear() + assert_array(empty).is_empty() + # should fail because the array is not empty + assert_failure(func() -> void: assert_array(array).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + '%s'""" + .dedent() + .trim_prefix("\n") % GdDefaultValueDecoder.decode(array)) + + +func test_is_not_empty(_test: String, array: Variant, _test_parameters := [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).is_not_empty() + # should fail because the array is empty + var empty :Variant = array.duplicate() + empty.clear() + assert_failure(func() -> void: assert_array(empty).is_not_empty()) \ + .is_failed() \ + .has_message("Expecting:\n must not be empty") + + +func test_is_same(value: Variant, _test_parameters := [ + [[0]], + [PackedByteArray([0])], + [PackedFloat32Array([0.0])], + [PackedFloat64Array([0.0])], + [PackedInt32Array([0])], + [PackedInt64Array([0])], + [PackedStringArray([""])], + [PackedColorArray([Color.RED])], + [PackedVector2Array([Vector2.ZERO])], + [PackedVector3Array([Vector3.ZERO])], +]) -> void: + assert_array(value).is_same(value) + + var v := GdDefaultValueDecoder.decode(value) + assert_failure(func() -> void: assert_array(value).is_same(value.duplicate()))\ + .is_failed()\ + .has_message(""" + Expecting: + '%s' + to refer to the same object + '%s'""" + .dedent() + .trim_prefix("\n") % [v, v]) + + +func test_is_not_same(value: Variant, _test_parameters := [ + [[0]], + [PackedByteArray([0])], + [PackedFloat32Array([0.0])], + [PackedFloat64Array([0.0])], + [PackedInt32Array([0])], + [PackedInt64Array([0])], + [PackedStringArray([""])], + [PackedColorArray([Color.RED])], + [PackedVector2Array([Vector2.ZERO])], + [PackedVector3Array([Vector3.ZERO])], +]) -> void: + assert_array(value).is_not_same(value.duplicate()) + + assert_failure(func() -> void: assert_array(value).is_not_same(value))\ + .is_failed()\ + .has_message("Expecting not same:\n '%s'" % GdDefaultValueDecoder.decode(value)) + + +func test_has_size(_test: String, array: Variant, _test_parameters := [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).has_size(5) + # should fail because the array has a size of 5 + assert_failure(func() -> void: assert_array(array).has_size(4)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '4' + but was + '5'""" + .dedent() + .trim_prefix("\n")) + + +func test_contains(_test: String, array: Variant, _test_parameters := [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).contains([array[1], array[3], array[4]]) + # should fail because the array not contains 7 and 6 + var do_contains := [array[1], 7, 6] + assert_failure(func() -> void: assert_array(array).contains(do_contains)) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '$source' + do contains (in any order) + '$contains' + but could not find elements: + '[7, 6]'""" + .dedent() + .trim_prefix("\n") + .replace("$source", GdDefaultValueDecoder.decode(array)) + .replace("$contains", GdDefaultValueDecoder.decode(do_contains)) + ) + + +func test_contains_exactly(_test: String, array: Variant, _test_parameters := [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).contains_exactly(array.duplicate()) + # should fail because the array not contains same elements but in different order + var shuffled :Variant = array.duplicate() + shuffled[1] = array[3] + shuffled[3] = array[1] + assert_failure(func() -> void: assert_array(array).contains_exactly(shuffled)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '$source' + do contains (in same order) + '$contains' + but has different order at position '1' + '$A' vs '$B'""" + .dedent() + .trim_prefix("\n") + .replace("$A", GdDefaultValueDecoder.decode(array[1])) + .replace("$B", GdDefaultValueDecoder.decode(array[3])) + .replace("$source", GdDefaultValueDecoder.decode(array)) + .replace("$contains", GdDefaultValueDecoder.decode(shuffled)) + ) + + +func test_override_failure_message(_test: String, array: Variant, _test_parameters := [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + + assert_object(assert_array(array).override_failure_message("error")).is_instanceof(GdUnitArrayAssert) + assert_failure(func() -> void: assert_array(array) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_array([]).append_failure_message("error")).is_instanceof(GdUnitArrayAssert) + assert_failure(func() -> void: assert_array([]) \ + .append_failure_message("custom failure data") \ + .is_not_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must not be empty + Additional info: + custom failure data""".dedent().trim_prefix("\n")) diff --git a/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd.uid new file mode 100644 index 0000000..12ec4c0 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd.uid @@ -0,0 +1 @@ +uid://b86p17tljk70b diff --git a/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd new file mode 100644 index 0000000..0a6fed0 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd @@ -0,0 +1,158 @@ +# GdUnit generated TestSuite +class_name GdUnitResultAssertImplTest +extends GdUnitTestSuite + + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd' + + +func test_is_null() -> void: + assert_result(null).is_null() + + assert_failure(func() -> void: assert_result(GdUnitResult.success("")).is_null()) \ + .is_failed() \ + .has_message('Expecting: \'\' but was <{ "state": 0, "value": "\\"\\"", "warn_msg": "", "err_msg": "" }>') + + +func test_is_not_null() -> void: + assert_result(GdUnitResult.success("")).is_not_null() + + assert_failure(func() -> void: assert_result(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_empty() -> void: + assert_result(GdUnitResult.empty()).is_empty() + + assert_failure(func() -> void: assert_result(GdUnitResult.warn("a warning")).is_empty()) \ + .is_failed() \ + .has_message("Expecting the result must be a EMPTY but was WARNING:\n 'a warning'") + assert_failure(func() -> void: assert_result(GdUnitResult.error("a error")).is_empty()) \ + .is_failed() \ + .has_message("Expecting the result must be a EMPTY but was ERROR:\n 'a error'") + assert_failure(func() -> void: assert_result(null).is_empty()) \ + .is_failed() \ + .has_message("Expecting the result must be a EMPTY but was .") + + +func test_is_success() -> void: + assert_result(GdUnitResult.success("")).is_success() + + assert_failure(func() -> void: assert_result(GdUnitResult.warn("a warning")).is_success()) \ + .is_failed() \ + .has_message("Expecting the result must be a SUCCESS but was WARNING:\n 'a warning'") + assert_failure(func() -> void: assert_result(GdUnitResult.error("a error")).is_success()) \ + .is_failed() \ + .has_message("Expecting the result must be a SUCCESS but was ERROR:\n 'a error'") + assert_failure(func() -> void: assert_result(null).is_success()) \ + .is_failed() \ + .has_message("Expecting the result must be a SUCCESS but was .") + + +func test_is_warning() -> void: + assert_result(GdUnitResult.warn("a warning")).is_warning() + + assert_failure(func() -> void: assert_result(GdUnitResult.success("value")).is_warning()) \ + .is_failed() \ + .has_message("Expecting the result must be a WARNING but was SUCCESS.") + assert_failure(func() -> void: assert_result(GdUnitResult.error("a error")).is_warning()) \ + .is_failed() \ + .has_message("Expecting the result must be a WARNING but was ERROR:\n 'a error'") + assert_failure(func() -> void: assert_result(null).is_warning()) \ + .is_failed() \ + .has_message("Expecting the result must be a WARNING but was .") + + +func test_is_error() -> void: + assert_result(GdUnitResult.error("a error")).is_error() + + assert_failure(func() -> void: assert_result(GdUnitResult.success("")).is_error()) \ + .is_failed() \ + .has_message("Expecting the result must be a ERROR but was SUCCESS.") + assert_failure(func() -> void: assert_result(GdUnitResult.warn("a warning")).is_error()) \ + .is_failed() \ + .has_message("Expecting the result must be a ERROR but was WARNING:\n 'a warning'") + assert_failure(func() -> void: assert_result(null).is_error()) \ + .is_failed() \ + .has_message("Expecting the result must be a ERROR but was .") + + +func test_contains_message() -> void: + assert_result(GdUnitResult.error("a error")).contains_message("a error") + assert_result(GdUnitResult.warn("a warning")).contains_message("a warning") + + assert_failure(func() -> void: assert_result(GdUnitResult.success("")).contains_message("Error 500")) \ + .is_failed() \ + .has_message("Expecting:\n 'Error 500'\n but the GdUnitResult is a success.") + assert_failure(func() -> void: assert_result(GdUnitResult.warn("Warning xyz!")).contains_message("Warning aaa!")) \ + .is_failed() \ + .has_message("Expecting:\n 'Warning aaa!'\n but was\n 'Warning xyz!'.") + assert_failure(func() -> void: assert_result(GdUnitResult.error("Error 410")).contains_message("Error 500")) \ + .is_failed() \ + .has_message("Expecting:\n 'Error 500'\n but was\n 'Error 410'.") + assert_failure(func() -> void: assert_result(null).contains_message("Error 500")) \ + .is_failed() \ + .has_message("Expecting:\n 'Error 500'\n but was\n ''.") + + +func test_is_value() -> void: + assert_result(GdUnitResult.success("")).is_value("") + var result_value :Node = auto_free(Node.new()) + assert_result(GdUnitResult.success(result_value)).is_value(result_value) + + assert_failure(func() -> void: assert_result(GdUnitResult.success("")).is_value("abc")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n 'abc'\n but was\n ''.") + assert_failure(func() -> void: assert_result(GdUnitResult.success("abc")).is_value("")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n ''\n but was\n 'abc'.") + assert_failure(func() -> void: assert_result(GdUnitResult.success(result_value)).is_value("")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n ''\n but was\n .") + assert_failure(func() -> void: assert_result(null).is_value("")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n ''\n but was\n ''.") + + +func test_override_failure_message() -> void: + assert_object(assert_result(GdUnitResult.success("")).override_failure_message("error")).is_instanceof(GdUnitResultAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_result(GdUnitResult.success("")) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_result(GdUnitResult.success("")).append_failure_message("error")).is_instanceof(GdUnitResultAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_result(GdUnitResult.success("")) \ + .append_failure_message("custom failure data") \ + .is_error()) \ + .is_failed() \ + .has_message(""" + Expecting the result must be a ERROR but was SUCCESS. + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_result(null).is_null() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_result(RefCounted.new()).is_null()).is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_result(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() diff --git a/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd.uid new file mode 100644 index 0000000..da6b54d --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://cynlvacv8kcta diff --git a/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd new file mode 100644 index 0000000..2a3e24f --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd @@ -0,0 +1,316 @@ +@warning_ignore_start("redundant_await") +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitSignalAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd' +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +class TestEmitter extends Node: + signal test_signal_counted(value: int) + signal test_signal(value: int) + signal test_signal_without_value() + signal test_signal_a(value: String) + signal test_signal_arry_value(values: Array) + @warning_ignore("unused_signal") + signal test_signal_unused() + + var _trigger_count :int + var _count := 0 + + func _init(trigger_count := 10) -> void: + _trigger_count = trigger_count + + func _process(_delta :float) -> void: + if _count >= _trigger_count: + test_signal_counted.emit(_count) + + if _count == 20: + test_signal.emit(10) + test_signal.emit(20) + test_signal_a.emit("foo") + test_signal_without_value.emit() + test_signal_arry_value.emit(1, 2, 3) + _count += 1 + + func reset_trigger(trigger_count := 10) -> void: + _trigger_count = trigger_count + _count = 0 + + +var signal_emitter: TestEmitter + + +func before_test() -> void: + signal_emitter = auto_free(TestEmitter.new()) + add_child(signal_emitter) + + +func test_invalid_arg() -> void: + ( + await assert_failure_await(func() -> void: await assert_signal(null).wait_until(50).is_emitted("test_signal_counted")) + ).has_message("Can't wait for signal checked a NULL object.") + ( + await assert_failure_await(func() -> void: await assert_signal(null).wait_until(50).is_not_emitted("test_signal_counted")) + ).has_message("Can't wait for signal checked a NULL object.") + + +func test_unknown_signal() -> void: + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(50).is_emitted("unknown")) + ).has_message("Can't wait for non-existion signal 'unknown' checked object 'Node'.") + + +func test_signal_is_emitted_by_signal_name() -> void: + await assert_signal(signal_emitter).is_emitted("test_signal_without_value") + await assert_signal(signal_emitter).is_emitted("test_signal_counted", 20) + + +func test_signal_is_emitted_by_signal_type() -> void: + await assert_signal(signal_emitter).is_emitted(signal_emitter.test_signal_without_value) + await assert_signal(signal_emitter).is_emitted(signal_emitter.test_signal_counted, 20) + + +func test_signal_is_emitted_by_invalid_type() -> void: + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).is_emitted(10)) + ).has_message("Invalid signal_name: expected String or Signal, but is 'int'") + + +func test_signal_is_emitted_without_args() -> void: + # wait until signal 'test_signal_counted' without args + await assert_signal(signal_emitter).is_emitted("test_signal_without_value") + # wait until signal 'test_signal_unused' where is never emitted + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(500).is_emitted("test_signal_unused")) + ).has_message("Expecting emit signal: 'test_signal_unused()' but timed out after 500ms") + + +func test_signal_is_emitted_with_array_args() -> void: + # wait until signal 'test_signal_counted' is emitted with value 20 + await assert_signal(signal_emitter).is_emitted("test_signal_counted", [20]) + + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(50).is_emitted("test_signal_counted", [500])) + ).has_message("Expecting emit signal: 'test_signal_counted([500])' but timed out after 50ms") + + +func test_signal_is_emitted_with_variadic_args() -> void: + await assert_signal(signal_emitter).is_emitted("test_signal_counted", 20) + + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(50).is_emitted("test_signal_counted", 500)) + ).has_message("Expecting emit signal: 'test_signal_counted([500])' but timed out after 50ms") + + +func test_signal_is_emitted_with_any_args() -> void: + await assert_signal(signal_emitter).is_emitted("test_signal_counted", any()) + + +func test_signal_is_emitted_use_argument_matcher() -> void: + # wait until signal 'test_signal_counted' is emitted by using any_int() matcher for signal arguments + await assert_signal(signal_emitter).is_emitted("test_signal_counted", [any_int()]) + + # should also work with any() matcher + signal_emitter.reset_trigger() + await assert_signal(signal_emitter).is_emitted("test_signal_counted", [any()]) + + # should fail because the matcher uses the wrong type + signal_emitter.reset_trigger() + ( + await assert_failure_await( func() -> void: await assert_signal(signal_emitter).wait_until(50).is_emitted("test_signal_counted", [any_string()])) + ).has_message("Expecting emit signal: 'test_signal_counted([any_string()])' but timed out after 50ms") + + +func test_signal_is_not_emitted_by_signal_name() -> void: + await assert_signal(signal_emitter).wait_until(50).is_not_emitted("test_signal_counted") + await assert_signal(signal_emitter).wait_until(30).is_not_emitted("test_signal_counted", 50) + + +func test_signal_is_not_emitted_by_signal_type() -> void: + await assert_signal(signal_emitter).wait_until(50).is_not_emitted(signal_emitter.test_signal_counted) + await assert_signal(signal_emitter).wait_until(30).is_not_emitted(signal_emitter.test_signal_counted, 50) + + +func test_signal_is_not_emitted() -> void: + # wait to verify signal 'test_signal_counted()' is not emitted until the first 50 ms + await assert_signal(signal_emitter).wait_until(50).is_not_emitted("test_signal_counted") + # wait to verify signal 'test_signal_counted(50)' is not emitted until the NEXT 30 ms + await assert_signal(signal_emitter).wait_until(30).is_not_emitted("test_signal_counted", [50]) + + # until the next 500 ms the signal is emitted and ends in a failure + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(1000).is_not_emitted("test_signal_counted", [50])) + ).starts_with_message("Expecting do not emit signal: 'test_signal_counted([50])' but is emitted after") + + +func test_signal_is_not_emitted_use_varargs() -> void: + # wait to verify signal 'test_signal_counted()' is not emitted until the first 50 ms + await assert_signal(signal_emitter).wait_until(50).is_not_emitted("test_signal_counted") + # wait to verify signal 'test_signal_counted(50)' is not emitted until the NEXT 30 ms + await assert_signal(signal_emitter).wait_until(30).is_not_emitted("test_signal_counted", 50) + + # until the next 500 ms the signal is emitted and ends in a failure + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(1000).is_not_emitted("test_signal_counted", 50)) + ).starts_with_message("Expecting do not emit signal: 'test_signal_counted([50])' but is emitted after") + + +func test_signal_is_not_emitted_use_argument_matcher() -> void: + # wait until signal 'test_signal_counted' is NOT emitted by using any_int() matcher for signal arguments + await assert_signal(signal_emitter).wait_until(10).is_not_emitted("test_signal_counted", [any()]) + await assert_signal(signal_emitter).wait_until(10).is_not_emitted("test_signal_counted", [any_int()]) + + # until the next 500ms the signal is emitted and ends in a failure + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(1000).is_not_emitted("test_signal_counted", [any()])) + ).starts_with_message("Expecting do not emit signal: 'test_signal_counted([any()])' but is emitted after") + + +func test_signal_is_not_emitted_use_variadic_argument_matcher() -> void: + # wait until signal 'test_signal_counted' is NOT emitted by using any_int() matcher for signal arguments + await assert_signal(signal_emitter).wait_until(10).is_not_emitted("test_signal_counted", any()) + await assert_signal(signal_emitter).wait_until(10).is_not_emitted("test_signal_counted", any_int()) + + # until the next 500ms the signal is emitted and ends in a failure + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter).wait_until(1000).is_not_emitted("test_signal_counted", any())) + ).starts_with_message("Expecting do not emit signal: 'test_signal_counted([any()])' but is emitted after") + + +func test_signal_is_not_emitted_use_argument_matcher_GD_878() -> void: + await assert_signal(signal_emitter).wait_until(500).is_emitted("test_signal_a", ["foo"]) + # verify the signal is not emitted again + await assert_signal(signal_emitter).wait_until(10).is_not_emitted("test_signal_a", ["foo"]) + + # restart trigger counter + signal_emitter.reset_trigger() + await assert_signal(signal_emitter).wait_until(500).is_emitted("test_signal_a", [any()]) + # verify the signal is not emitted again + await assert_signal(signal_emitter).wait_until(1).is_not_emitted("test_signal_a", [any()]) + + +func test_override_failure_message() -> void: + assert_object(assert_signal(signal_emitter).override_failure_message("error")).is_instanceof(GdUnitSignalAssert) + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter) \ + .override_failure_message("Custom failure message")\ + .wait_until(100)\ + .is_emitted("test_signal_unused")) + ).has_message("Custom failure message") + + +@warning_ignore("unsafe_method_access") +func test_append_failure_message() -> void: + assert_object(assert_signal(signal_emitter).append_failure_message("error")).is_instanceof(GdUnitSignalAssert) + ( + await assert_failure_await(func() -> void: await assert_signal(signal_emitter) \ + .append_failure_message("custom failure data")\ + .wait_until(100)\ + .is_emitted("test_signal_unused")) + ).has_message(""" + Expecting emit signal: 'test_signal_unused()' but timed out after 100ms + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +func test_node_changed_emitting_signals() -> void: + var node :Node2D = auto_free(Node2D.new()) + add_child(node) + + await assert_signal(node).wait_until(200).is_emitted("draw") + + node.visible = false; + await assert_signal(node).wait_until(200).is_emitted("visibility_changed") + + # expecting to fail, we not changed the visibility + #node.visible = true; + ( + await assert_failure_await(func() -> void: await assert_signal(node).wait_until(200).is_emitted("visibility_changed")) + ).has_message("Expecting emit signal: 'visibility_changed()' but timed out after 200ms") + + node.show() + await assert_signal(node).wait_until(200).is_emitted("draw") + + +func test_is_signal_exists() -> void: + var node: Node2D = auto_free(Node2D.new()) + + assert_signal(node).is_signal_exists("visibility_changed")\ + .is_signal_exists("draw")\ + .is_signal_exists("visibility_changed")\ + .is_signal_exists("tree_entered")\ + .is_signal_exists(node.tree_exiting)\ + .is_signal_exists(node.tree_exited) + + ( + await assert_failure_await(func() -> void: assert_signal(node).is_signal_exists("not_existing_signal")) + ).has_message("The signal 'not_existing_signal' not exists checked object 'Node2D'.") + + +class MyEmitter extends Node: + + signal my_signal_a + signal my_signal_b(value :String) + + + func do_emit_a() -> void: + my_signal_a.emit() + + + func do_emit_b() -> void: + my_signal_b.emit("foo") + + +@warning_ignore("unsafe_method_access") +func test_monitor_signals() -> void: + # start to watch on the emitter to collect all emitted signals + var emitter_a: MyEmitter = monitor_signals(MyEmitter.new()) + var emitter_b: MyEmitter = monitor_signals(MyEmitter.new()) + + # verify the signals are not emitted initial + await assert_signal(emitter_a).wait_until(50).is_not_emitted('my_signal_a') + await assert_signal(emitter_a).wait_until(50).is_not_emitted('my_signal_b') + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_a') + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_b') + + # emit signal `my_signal_a` on emitter_a + emitter_a.do_emit_a() + await assert_signal(emitter_a).is_emitted('my_signal_a') + + # emit signal `my_signal_b` on emitter_a + emitter_a.do_emit_b() + await assert_signal(emitter_a).is_emitted('my_signal_b', ["foo"]) + # verify emitter_b still has nothing emitted + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_a') + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_b') + + # now verify emitter b + emitter_b.do_emit_a() + await assert_signal(emitter_b).wait_until(50).is_emitted('my_signal_a') + + +class ExampleResource extends Resource: + @export var title := "Title": + set(new_value): + title = new_value + changed.emit() + + + func change_title(p_title: String) -> void: + title = p_title + + +func test_monitor_signals_on_resource_set() -> void: + var sut := ExampleResource.new() + var emitter := monitor_signals(sut) + + sut.change_title("Some title") + + # title change should emit "changed" signal + await assert_signal(emitter).is_emitted("changed") + assert_str(sut.title).is_equal("Some title") diff --git a/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd.uid new file mode 100644 index 0000000..bd5f3ce --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://biqffedu42a7n diff --git a/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd new file mode 100644 index 0000000..df26251 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd @@ -0,0 +1,488 @@ +# GdUnit generated TestSuite +class_name GdUnitStringAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd' + + +func test_is_null() -> void: + assert_str(null).is_null() + + assert_failure(func() -> void: assert_str("abc").is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was 'abc'") + + +func test_is_not_null() -> void: + assert_str("abc").is_not_null() + assert_str(&"abc").is_not_null() + + assert_failure(func() -> void: assert_str(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_str("This is a test message").is_equal("This is a test message") + assert_str("abc").is_equal("abc") + assert_str("abc").is_equal(&"abc") + assert_str(&"abc").is_equal("abc") + assert_str(&"abc").is_equal(&"abc") + + assert_failure(func() -> void: assert_str("This is a test message").is_equal("This is a test Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test Message' + but was + 'This is a test Mmessage'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).is_equal("This is a test Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test Message' + but was + ''""".dedent().trim_prefix("\n")) + + +func test_is_equal_pipe_character() -> void: + assert_failure(func() -> void: assert_str("AAA|BBB|CCC").is_equal("AAA|BBB.CCC")) \ + .is_failed() + + +func test_is_equal_ignoring_case() -> void: + assert_str("This is a test message").is_equal_ignoring_case("This is a test Message") + assert_str("This is a test message").is_equal_ignoring_case(&"This is a test Message") + assert_str(&"This is a test message").is_equal_ignoring_case("This is a test Message") + assert_str(&"This is a test message").is_equal_ignoring_case(&"This is a test Message") + + assert_failure(func() -> void: assert_str("This is a test message").is_equal_ignoring_case("This is a Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a Message' + but was + 'This is a Mtest message' (ignoring case)""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).is_equal_ignoring_case("This is a Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a Message' + but was + '' (ignoring case)""".dedent().trim_prefix("\n")) + + +func test_is_not_equal() -> void: + assert_str(null).is_not_equal("This is a test Message") + assert_str("This is a test message").is_not_equal("This is a test Message") + assert_str("This is a test message").is_not_equal(&"This is a test Message") + assert_str(&"This is a test message").is_not_equal("This is a test Message") + assert_str(&"This is a test message").is_not_equal(&"This is a test Message") + + assert_failure(func() -> void: assert_str("This is a test message").is_not_equal("This is a test message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not equal to + 'This is a test message'""".dedent().trim_prefix("\n")) + + +func test_is_not_equal_ignoring_case() -> void: + assert_str(null).is_not_equal_ignoring_case("This is a Message") + assert_str("This is a test message").is_not_equal_ignoring_case("This is a Message") + assert_str("This is a test message").is_not_equal_ignoring_case(&"This is a Message") + assert_str(&"This is a test message").is_not_equal_ignoring_case("This is a Message") + assert_str(&"This is a test message").is_not_equal_ignoring_case(&"This is a Message") + + assert_failure(func() -> void: assert_str("This is a test message").is_not_equal_ignoring_case("This is a test Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test Message' + not equal to + 'This is a test message'""".dedent().trim_prefix("\n")) + + +func test_is_empty() -> void: + assert_str("").is_empty() + assert_str(&"").is_empty() + + assert_failure(func() -> void: assert_str(" ").is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + ' '""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str("abc").is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + 'abc'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(&"abc").is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + 'abc'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + ''""".dedent().trim_prefix("\n")) + + +func test_is_not_empty() -> void: + assert_str(" ").is_not_empty() + assert_str(" ").is_not_empty() + assert_str("abc").is_not_empty() + assert_str(&"abc").is_not_empty() + + assert_failure(func() -> void: assert_str("").is_not_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must not be empty""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).is_not_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must not be empty""".dedent().trim_prefix("\n")) + + +func test_contains() -> void: + assert_str("This is a test message").contains("a test") + assert_str("This is a test message").contains(&"a test") + assert_str(&"This is a test message").contains("a test") + assert_str(&"This is a test message").contains(&"a test") + # must fail because of camel case difference + assert_failure(func() -> void: assert_str("This is a test message").contains("a Test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + do contains + 'a Test'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).contains("a Test")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + do contains + 'a Test'""".dedent().trim_prefix("\n")) + + +func test_not_contains() -> void: + assert_str(null).not_contains("a tezt") + assert_str("This is a test message").not_contains("a tezt") + assert_str("This is a test message").not_contains(&"a tezt") + assert_str(&"This is a test message").not_contains("a tezt") + assert_str(&"This is a test message").not_contains(&"a tezt") + + assert_failure(func() -> void: assert_str("This is a test message").not_contains("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not do contain + 'a test'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(&"This is a test message").not_contains("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not do contain + 'a test'""".dedent().trim_prefix("\n")) + + +func test_contains_ignoring_case() -> void: + assert_str("This is a test message").contains_ignoring_case("a Test") + assert_str("This is a test message").contains_ignoring_case(&"a Test") + assert_str(&"This is a test message").contains_ignoring_case("a Test") + assert_str(&"This is a test message").contains_ignoring_case(&"a Test") + + assert_failure(func() -> void: assert_str("This is a test message").contains_ignoring_case("a Tesd")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + contains + 'a Tesd' + (ignoring case)""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).contains_ignoring_case("a Tesd")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + contains + 'a Tesd' + (ignoring case)""".dedent().trim_prefix("\n")) + + +func test_not_contains_ignoring_case() -> void: + assert_str(null).not_contains_ignoring_case("a Test") + assert_str("This is a test message").not_contains_ignoring_case("a Tezt") + assert_str("This is a test message").not_contains_ignoring_case(&"a Tezt") + assert_str(&"This is a test message").not_contains_ignoring_case("a Tezt") + assert_str(&"This is a test message").not_contains_ignoring_case(&"a Tezt") + + assert_failure(func() -> void: assert_str("This is a test message").not_contains_ignoring_case("a Test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not do contains + 'a Test' + (ignoring case)""".dedent().trim_prefix("\n")) + + +func test_starts_with() -> void: + assert_str("This is a test message").starts_with("This is") + assert_str("This is a test message").starts_with(&"This is") + assert_str(&"This is a test message").starts_with("This is") + assert_str(&"This is a test message").starts_with(&"This is") + + assert_failure(func() -> void: assert_str("This is a test message").starts_with("This iss")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to start with + 'This iss'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str("This is a test message").starts_with("this is")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to start with + 'this is'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str("This is a test message").starts_with("test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to start with + 'test'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).starts_with("test")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + to start with + 'test'""".dedent().trim_prefix("\n")) + + +func test_ends_with() -> void: + assert_str("This is a test message").ends_with("test message") + assert_str("This is a test message").ends_with(&"test message") + assert_str(&"This is a test message").ends_with("test message") + assert_str(&"This is a test message").ends_with(&"test message") + + assert_failure(func() -> void: assert_str("This is a test message").ends_with("tes message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to end with + 'tes message'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str("This is a test message").ends_with("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to end with + 'a test'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).ends_with("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + to end with + 'a test'""".dedent().trim_prefix("\n")) + + +func test_has_length() -> void: + assert_str("This is a test message").has_length(22) + assert_str(&"This is a test message").has_length(22) + assert_str("").has_length(0) + assert_str(&"").has_length(0) + + assert_failure(func() -> void: assert_str("This is a test message").has_length(23)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '23' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).has_length(23)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '23' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_length_less_than() -> void: + assert_str("This is a test message").has_length(23, Comparator.LESS_THAN) + assert_str("This is a test message").has_length(42, Comparator.LESS_THAN) + assert_str(&"This is a test message").has_length(42, Comparator.LESS_THAN) + + assert_failure(func() -> void: assert_str("This is a test message").has_length(22, Comparator.LESS_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than: + '22' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).has_length(22, Comparator.LESS_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than: + '22' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_length_less_equal() -> void: + assert_str("This is a test message").has_length(22, Comparator.LESS_EQUAL) + assert_str("This is a test message").has_length(23, Comparator.LESS_EQUAL) + assert_str(&"This is a test message").has_length(23, Comparator.LESS_EQUAL) + + assert_failure(func() -> void: assert_str("This is a test message").has_length(21, Comparator.LESS_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than or equal: + '21' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).has_length(21, Comparator.LESS_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than or equal: + '21' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_length_greater_than() -> void: + assert_str("This is a test message").has_length(21, Comparator.GREATER_THAN) + assert_str(&"This is a test message").has_length(21, Comparator.GREATER_THAN) + + assert_failure(func() -> void: assert_str("This is a test message").has_length(22, Comparator.GREATER_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than: + '22' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).has_length(22, Comparator.GREATER_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than: + '22' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_length_greater_equal() -> void: + assert_str("This is a test message").has_length(21, Comparator.GREATER_EQUAL) + assert_str("This is a test message").has_length(22, Comparator.GREATER_EQUAL) + assert_str(&"This is a test message").has_length(22, Comparator.GREATER_EQUAL) + + assert_failure(func() -> void: assert_str("This is a test message").has_length(23, Comparator.GREATER_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than or equal: + '23' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func() -> void: assert_str(null).has_length(23, Comparator.GREATER_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than or equal: + '23' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_fluentable() -> void: + assert_str("value a").is_not_equal("a") \ + .is_equal("value a") \ + .has_length(7) \ + .is_equal("value a") + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func() -> void: assert_str(1)) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_str(1.3)) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_str(true)) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + assert_failure(func() -> void: assert_str(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_object(assert_str("").override_failure_message("error")).is_instanceof(GdUnitStringAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_str("") \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_str("").append_failure_message("error")).is_instanceof(GdUnitStringAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_str("") \ + .append_failure_message("custom failure data") \ + .is_not_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must not be empty + Additional info: + custom failure data""".dedent().trim_prefix("\n")) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_str(null).is_null() + assert_bool(is_failure()).is_false() + + # checked failed assert + assert_failure(func() -> void: assert_str(RefCounted.new()).is_null()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_str(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + +func test_ๆ—ฅๆœฌ่ชž() -> void: + assert_str("test_ๆ—ฅๆœฌ่ชž").is_equal("test_ๆ—ฅๆœฌ่ชž") + assert_failure(func() -> void: assert_str("test_ๆ—ฅๆœฌ่ชž").is_equal("test_ๆœฌ่ชž")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'test_ๆœฌ่ชž' + but was + 'test_ๆ—ฅๆœฌ่ชž'""".dedent().trim_prefix("\n")) + + +func test_is_equal_on_rich_text() -> void: + assert_failure(func() -> void: + assert_str("[color=ff00ff]test[/color]").is_equal("[color=ffff00]test[/color]") + ).is_failed() \ + # We expect for text containing bbcode tags is rendered with masked tags + .has_message(""" + Expecting: + '[lb]color=ffff00]test[lb]/color]' + but was + '[lb]color=ff00ff]test[lb]/color]'""".dedent().trim_prefix("\n")) diff --git a/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd.uid new file mode 100644 index 0000000..9ff9ad2 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://bkec5dqrgxn4l diff --git a/addons/gdUnit4/test/asserts/GdUnitVariantAssertThatTest.gd b/addons/gdUnit4/test/asserts/GdUnitVariantAssertThatTest.gd new file mode 100644 index 0000000..e01eff5 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitVariantAssertThatTest.gd @@ -0,0 +1,116 @@ +extends GdUnitTestSuite + + +func test_is_equal_success() -> void: + assert_that(3 as Variant).is_equal(3) + assert_that(3.14 as Variant).is_equal(3.14) + assert_that("3" as Variant).is_equal("3") + assert_that(true as Variant).is_equal(true) + assert_that(Vector2.ONE as Variant).is_equal(Vector2.ONE) + assert_that({a=1, b=2} as Variant).is_equal({a=1, b=2}) + assert_that([1,2,3] as Variant).is_equal([1,2,3]) + assert_that(RefCounted.new() as Variant).is_equal(RefCounted.new()) + + +func test_is_equal_fail() -> void: + # bool vs int + assert_failure(func()->void: + assert_that(true as Variant).is_equal(1))\ + .is_failed() + + # bool vs string + assert_failure(func()->void: + assert_that(true as Variant).is_equal("true"))\ + .is_failed() + + # int vs string + assert_failure(func()->void: + assert_that(3 as Variant).is_equal("3"))\ + .is_failed() + + # float vs string + assert_failure(func()->void: + assert_that(3.14 as Variant).is_equal("3.14"))\ + .is_failed() + + # string vs int + assert_failure(func()->void: + assert_that("3" as Variant).is_equal(3))\ + .is_failed() + + # string vs float + assert_failure(func()->void: + assert_that("3.14" as Variant).is_equal(3.14))\ + .is_failed() + + # vector vs string + assert_failure(func()->void: + assert_that(Vector2.ONE as Variant).is_equal("ONE"))\ + .is_failed() + + # dictionary vs string + assert_failure(func()->void: + assert_that({a=1, b=2} as Variant).is_equal("FOO"))\ + .is_failed() + + # array vs string + assert_failure(func()->void: + assert_that([1,2,3] as Variant).is_equal("FOO"))\ + .is_failed() + + # object vs string + assert_failure(func()->void: + assert_that(RefCounted.new() as Variant).is_equal("FOO"))\ + .is_failed() + + +func test_is_not_equal_success() -> void: + assert_that(3 as Variant).is_not_equal(4) + assert_that(3.14 as Variant).is_not_equal(3.15) + assert_that("3" as Variant).is_not_equal("33") + assert_that(true as Variant).is_not_equal(false) + assert_that(Vector2.ONE as Variant).is_not_equal(Vector2.UP) + assert_that({a=1, b=2} as Variant).is_not_equal({a=1, b=3}) + assert_that([1,2,3] as Variant).is_not_equal([1,2,4]) + assert_that(RefCounted.new() as Variant).is_not_equal(null) + + +func test_is_not_equal_fail() -> void: + # bool vs int + assert_failure(func()->void: + assert_that(true as Variant).is_not_equal(true) + )\ + .is_failed() + + + assert_failure(func()->void: + assert_that(3 as Variant).is_not_equal(3))\ + .is_failed() + + assert_failure(func()->void: + assert_that(3.14 as Variant).is_not_equal(3.14))\ + .is_failed() + + assert_failure(func()->void: + assert_that("3" as Variant).is_not_equal("3"))\ + .is_failed() + + # vector vs string + assert_failure(func()->void: + assert_that(Vector2.ONE as Variant).is_not_equal(Vector2.ONE))\ + .is_failed() + + # dictionary vs string + assert_failure(func()->void: + assert_that({a=1, b=2} as Variant).is_not_equal({a=1, b=2}))\ + .is_failed() + + # array vs string + assert_failure(func()->void: + assert_that([1,2,3] as Variant).is_not_equal([1,2,3]))\ + .is_failed() + + # object vs string + assert_failure(func()->void: + assert_that(RefCounted.new() as Variant).is_not_equal(RefCounted.new()))\ + .is_failed() diff --git a/addons/gdUnit4/test/asserts/GdUnitVariantAssertThatTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitVariantAssertThatTest.gd.uid new file mode 100644 index 0000000..fe633e2 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitVariantAssertThatTest.gd.uid @@ -0,0 +1 @@ +uid://bkqolhf4r6o86 diff --git a/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd new file mode 100644 index 0000000..c7d2ec4 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd @@ -0,0 +1,443 @@ +# GdUnit generated TestSuite +class_name GdUnitVectorAssertImplTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd' + + +var _test_seta := [ + [null], + [Vector2.ONE], + [Vector2i.ONE], + [Vector3.ONE], + [Vector3i.ONE], + [Vector4.ONE], + [Vector4i.ONE], +] + + +@warning_ignore("unused_parameter") +func test_supported_types(value :Variant, test_parameters := _test_seta) -> void: + assert_object(assert_vector(value))\ + .is_not_null()\ + .is_instanceof(GdUnitVectorAssert) + + +@warning_ignore("unused_parameter") +func test_unsupported_types(value :Variant, details :String, test_parameters :=[ + [true, 'bool'], + [42, 'int'], + [42.0, 'float'], + ['foo', 'String'], +] ) -> void: + assert_failure(func() -> void: assert_vector(value))\ + .is_failed()\ + .has_message("GdUnitVectorAssert error, the type <%s> is not supported." % details) + + +@warning_ignore("unused_parameter") +func test_is_null(value :Variant, test_parameters := _test_seta) -> void: + if value == null: + assert_vector(null).is_null() + else: + assert_failure(func() -> void: assert_vector(value).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was '%s'" % str(value)) + + +@warning_ignore("unused_parameter") +func test_is_not_null(value :Variant, test_parameters := _test_seta) -> void: + if value == null: + assert_failure(func() -> void: assert_vector(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + else: + assert_vector(value).is_not_null() + + +@warning_ignore("unused_parameter") +func test_is_equal() -> void: + assert_vector(Vector2.ONE).is_equal(Vector2.ONE) + assert_vector(Vector2.LEFT).is_equal(Vector2.LEFT) + assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001)) + + # is not equal + assert_failure(func() -> void: assert_vector(Vector2.ONE).is_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting:\n '$v0'\n but was\n '$v1'" + .replace("$v0", str(Vector2(1.2, 1.000001))) + .replace("$v1", str(Vector2.ONE)) + ) + # is null + assert_failure(func() -> void: assert_vector(null).is_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting:\n '$v0'\n but was\n ''" + .replace("$v0", str(Vector2(1.2, 1.000001))) + ) + # comparing different vector types + assert_failure(func() -> void: assert_vector(Vector2.ONE).is_equal(Vector3.ONE)) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_equal_over_all_types(value :Variant, test_parameters := _test_seta) -> void: + assert_vector(value).is_equal(value) + + +func test_is_not_equal() -> void: + assert_vector(null).is_not_equal(Vector2.LEFT) + assert_vector(Vector2.ONE).is_not_equal(Vector2.LEFT) + assert_vector(Vector2.LEFT).is_not_equal(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_not_equal(Vector2(1.2, 1.000002)) + + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000001)).is_not_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting:\n '(1.2, 1.000001)'\n not equal to\n '(1.2, 1.000001)'") + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000001)).is_not_equal(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_not_equal_over_all_types(value :Variant, test_parameters := _test_seta) -> void: + var expected :Variant = Vector2.LEFT if value == null else value * 2 + assert_vector(value).is_not_equal(expected) + + +func test_is_equal_approx() -> void: + assert_vector(Vector2.ONE).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004)) + assert_vector(Vector2(0.996, 0.996)).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004)) + assert_vector(Vector2(1.004, 1.004)).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004)) + + var current := Vector2(1.005, 1) + var approx_min := Vector2.ONE - Vector2(0.004, 0.004) + var approx_max := Vector2.ONE + Vector2(0.004, 0.004) + assert_failure(func() -> void: assert_vector(Vector2(1.005, 1)).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004))) \ + .is_failed() \ + .has_message("Expecting:\n '$v0'\n in range between\n '$v1' <> '$v2'" + .replace("$v0", str(current)) + .replace("$v1", str(approx_min)) + .replace("$v2", str(approx_max)) + ) + + current = Vector2(1, 0.995) + approx_min = Vector2.ONE - Vector2(0, 0.004) + approx_max = Vector2.ONE + Vector2(0, 0.004) + assert_failure(func() -> void: assert_vector(current).is_equal_approx(Vector2.ONE, Vector2(0, 0.004))) \ + .is_failed() \ + .has_message("Expecting:\n '$v0'\n in range between\n '$v1' <> '$v2'" + .replace("$v0", str(current)) + .replace("$v1", str(approx_min)) + .replace("$v2", str(approx_max)) + ) + + assert_failure(func() -> void: assert_vector(null).is_equal_approx(Vector2.ONE, Vector2(0, 0.004))) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '$v1' <> '$v2'" + .replace("$v1", str(approx_min)) + .replace("$v2", str(approx_max)) + ) + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000001)).is_equal_approx(Vector3.ONE, Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + assert_failure(func() -> void: assert_vector(Vector2(0.878431, 0.505882)).is_equal_approx(Vector2(0.878431, 0.105882), Vector2(0.000001, 0.000001))) \ + .is_failed() \ + .has_message(""" + Expecting: + '(0.878431, 0.505882)' + in range between + '(0.87843, 0.105881)' <> '(0.878432, 0.105883)'""" + .dedent().trim_prefix("\n") + ) + var currentV3 := Vector3(0.0, 0.878431, 0.505882) + assert_failure(func() -> void: assert_vector(currentV3).is_equal_approx(Vector3(0.0, 0.878431, 0.105882), Vector3(0.000001, 0.000001, 0.000001))) \ + .is_failed() \ + .has_message(""" + Expecting: + '$v0' + in range between + '(-0.000001, 0.87843, 0.105881)' <> '(0.000001, 0.878432, 0.105883)'""" + .dedent().trim_prefix("\n") + .replace("$v0", str(currentV3)) + ) + + +@warning_ignore("unused_parameter") +func test_is_equal_approx_over_all_types(value :Variant, expected :Variant, approx :Variant, test_parameters := [ + [Vector2(0.996, 1.004), Vector2.ONE, Vector2(0.004, 0.004)], + [Vector2i(9, 11), Vector2i(10, 10), Vector2i(1, 1)], + [Vector3(0.996, 0.996, 1.004), Vector3.ONE, Vector3(0.004, 0.004, 0.004)], + [Vector3i(10, 9, 11), Vector3i(10, 10, 10), Vector3i(1, 1, 1)], + [Vector4(0.996, 0.996, 1.004, 1.004), Vector4.ONE, Vector4(0.004, 0.004, 0.004, 0.004)], + [Vector4i(10, 9, 11, 9), Vector4i(10, 10, 10, 10), Vector4i(1, 1, 1, 1)] +]) -> void: + assert_vector(value).is_equal_approx(expected, approx) + + +func test_is_less() -> void: + assert_vector(Vector2.LEFT).is_less(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_less(Vector2(1.2, 1.000002)) + + assert_failure(func() -> void: assert_vector(Vector2.ONE).is_less(Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '$v0' but was '$v1'" + .replace("$v0", str(Vector2.ONE)) + .replace("$v1", str(Vector2.ONE)) + ) + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000001)).is_less(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '(1.2, 1.000001)' but was '(1.2, 1.000001)'") + assert_failure(func() -> void: assert_vector(null).is_less(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '(1.2, 1.000001)' but was ''") + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000001)).is_less(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_less_over_all_types(value :Variant, expected :Variant, test_parameters := [ + [Vector2(1.0, 1.0), Vector2(1.0001, 1.0001)], + [Vector2i(1, 1), Vector2i(2, 1)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0001, 1.0001, 1.0)], + [Vector3i(1, 1, 1), Vector3i(2, 1, 1)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0001, 1.0001, 1.0, 1.0)], + [Vector4i(1, 1, 1, 1), Vector4i(2, 1, 1, 1)], +]) -> void: + assert_vector(value).is_less(expected) + + +func test_is_less_equal() -> void: + assert_vector(Vector2.ONE).is_less_equal(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_less_equal(Vector2(1.2, 1.000001)) + assert_vector(Vector2(1.2, 1.000001)).is_less_equal(Vector2(1.2, 1.000002)) + + assert_failure(func() -> void: assert_vector(Vector2.ONE).is_less_equal(Vector2.ZERO)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '$v0' but was '$v1'" + .replace("$v0", str(Vector2.ZERO)) + .replace("$v1", str(Vector2.ONE)) + ) + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000002)).is_less_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '(1.2, 1.000001)' but was '(1.2, 1.000002)'") + assert_failure(func() -> void: assert_vector(null).is_less_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '(1.2, 1.000001)' but was ''") + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000002)).is_less_equal(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_less_equal_over_all_types(value :Variant, expected :Variant, test_parameters := [ + [Vector2(1.0, 1.0), Vector2(1.0001, 1.0001)], + [Vector2(1.0, 1.0), Vector2(1.0, 1.0)], + [Vector2i(1, 1), Vector2i(2, 1)], + [Vector2i(1, 1), Vector2i(1, 1)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0001, 1.0001, 1.0)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3i(1, 1, 1), Vector3i(2, 1, 1)], + [Vector3i(1, 1, 1), Vector3i(1, 1, 1)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0001, 1.0001, 1.0, 1.0)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4i(1, 1, 1, 1), Vector4i(2, 1, 1, 1)], + [Vector4i(1, 1, 1, 1), Vector4i(1, 1, 1, 1)], +]) -> void: + assert_vector(value).is_less_equal(expected) + + +func test_is_greater() -> void: + assert_vector(Vector2.ONE).is_greater(Vector2.RIGHT) + assert_vector(Vector2(1.2, 1.000002)).is_greater(Vector2(1.2, 1.000001)) + + assert_failure(func() -> void: assert_vector(Vector2.ZERO).is_greater(Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '$v0' but was '$v1'" + .replace("$v0", str(Vector2.ONE)) + .replace("$v1", str(Vector2.ZERO)) + ) + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000001)).is_greater(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '(1.2, 1.000001)' but was '(1.2, 1.000001)'") + assert_failure(func() -> void: assert_vector(null).is_greater(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '(1.2, 1.000001)' but was ''") + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000001)).is_greater(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_greater_over_all_types(value :Variant, expected :Variant, test_parameters := [ + [Vector2(1.0001, 1.0001), Vector2(1.0, 1.0)], + [Vector2i(2, 1), Vector2i(1, 1)], + [Vector3(1.0001, 1.0001, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3i(2, 1, 1), Vector3i(1, 1, 1)], + [Vector4(1.0001, 1.0001, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4i(2, 1, 1, 1), Vector4i(1, 1, 1, 1)], +]) -> void: + assert_vector(value).is_greater(expected) + + +func test_is_greater_equal() -> void: + assert_vector(Vector2.ONE*2).is_greater_equal(Vector2.ONE) + assert_vector(Vector2.ONE).is_greater_equal(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_greater_equal(Vector2(1.2, 1.000001)) + assert_vector(Vector2(1.2, 1.000002)).is_greater_equal(Vector2(1.2, 1.000001)) + + assert_failure(func() -> void: assert_vector(Vector2.ZERO).is_greater_equal(Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '$v0' but was '$v1'" + .replace("$v0", str(Vector2.ONE)) + .replace("$v1", str(Vector2.ZERO)) + ) + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000002)).is_greater_equal(Vector2(1.2, 1.000003))) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '(1.2, 1.000003)' but was '(1.2, 1.000002)'") + assert_failure(func() -> void: assert_vector(null).is_greater_equal(Vector2(1.2, 1.000003))) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '(1.2, 1.000003)' but was ''") + assert_failure(func() -> void: assert_vector(Vector2(1.2, 1.000002)).is_greater_equal(Vector3(1.2, 1.000003, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_greater_equal_over_all_types(value :Variant, expected :Variant, test_parameters := [ + [Vector2(1.0001, 1.0001), Vector2(1.0, 1.0)], + [Vector2(1.0, 1.0), Vector2(1.0, 1.0)], + [Vector2i(2, 1), Vector2i(1, 1)], + [Vector2i(1, 1), Vector2i(1, 1)], + [Vector3(1.0001, 1.0001, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3i(2, 1, 1), Vector3i(1, 1, 1)], + [Vector3i(1, 1, 1), Vector3i(1, 1, 1)], + [Vector4(1.0001, 1.0001, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4i(2, 1, 1, 1), Vector4i(1, 1, 1, 1)], + [Vector4i(1, 1, 1, 1), Vector4i(1, 1, 1, 1)], +]) -> void: + assert_vector(value).is_greater_equal(expected) + + +@warning_ignore("unused_parameter") +func test_is_between(fuzzer := Fuzzers.rangev2(Vector2.ZERO, Vector2.ONE), fuzzer_iterations := 200) -> void: + var value :Vector2 = fuzzer.next_value() + assert_vector(value).is_between(Vector2.ZERO, Vector2.ONE) + + +func test_is_between_failed() -> void: + var current := Vector2(1, 1.00001) + assert_failure(func() -> void: assert_vector(current).is_between(Vector2.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting:\n '$v0'\n in range between\n '$v1' <> '$v2'" + .replace("$v0", str(current)) + .replace("$v1", str(Vector2.ZERO)) + .replace("$v2", str(Vector2.ONE)) + ) + assert_failure(func() -> void: assert_vector(null).is_between(Vector2.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '$v0' <> '$v1'" + .replace("$v0", str(Vector2.ZERO)) + .replace("$v1", str(Vector2.ONE)) + ) + assert_failure(func() -> void: assert_vector(current).is_between(Vector2.ZERO, Vector3.ONE)) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_between_over_all_types(value :Variant, from :Variant, to :Variant, test_parameters := [ + [Vector2(1.2, 1.2), Vector2(1.0, 1.0), Vector2(1.2, 1.2)], + [Vector2i(1, 1), Vector2i(1, 1), Vector2i(2, 2)], + [Vector3(1.2, 1.2, 1.2), Vector3(1.0, 1.0, 1.0), Vector3(1.2, 1.2, 1.2)], + [Vector3i(1, 1, 1), Vector3i(1, 1, 1), Vector3i(2, 2, 2)], + [Vector4(1.2, 1.2, 1.2, 1.2), Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.2, 1.2, 1.2, 1.2)], + [Vector4i(1, 1, 1, 1), Vector4i(1, 1, 1, 1), Vector4i(2, 2, 2, 2)], +]) -> void: + assert_vector(value).is_between(from, to) + + +@warning_ignore("unused_parameter") +func test_is_not_between(fuzzer := Fuzzers.rangev2(Vector2.ONE, Vector2.ONE*2), fuzzer_iterations := 200) -> void: + var value :Vector2 = fuzzer.next_value() + assert_vector(null).is_not_between(Vector2.ZERO, Vector2.ONE) + assert_vector(value).is_not_between(Vector2.ZERO, Vector2.ONE) + + assert_failure(func() -> void: assert_vector(Vector2.ONE).is_not_between(Vector2.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting:\n '$v0'\n not in range between\n '$v1' <> '$v2'" + .replace("$v0", str(Vector2.ONE)) + .replace("$v1", str(Vector2.ZERO)) + .replace("$v2", str(Vector2.ONE)) + ) + assert_failure(func() -> void: assert_vector(Vector2.ONE).is_not_between(Vector3.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_not_between_over_all_types(value :Variant, from :Variant, to :Variant, test_parameters := [ + [Vector2(3.2, 1.2), Vector2(1.0, 1.0), Vector2(1.2, 1.2)], + [Vector2i(3, 1), Vector2i(1, 1), Vector2i(2, 2)], + [Vector3(3.2, 1.2, 1.2), Vector3(1.0, 1.0, 1.0), Vector3(1.2, 1.2, 1.2)], + [Vector3i(3, 1, 1), Vector3i(1, 1, 1), Vector3i(2, 2, 2)], + [Vector4(3.2, 1.2, 1.2, 1.2), Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.2, 1.2, 1.2, 1.2)], + [Vector4i(3, 1, 1, 1), Vector4i(1, 1, 1, 1), Vector4i(2, 2, 2, 2)], +]) -> void: + assert_vector(value).is_not_between(from, to) + + +func test_override_failure_message() -> void: + assert_object(assert_vector(Vector2.ONE).override_failure_message("error")).is_instanceof(GdUnitVectorAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_vector(Vector2.ONE) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +func test_append_failure_message() -> void: + assert_object(assert_vector(Vector2.ONE).append_failure_message("error")).is_instanceof(GdUnitVectorAssert) + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: assert_vector(Vector2.ONE) \ + .append_failure_message("custom failure data") \ + .is_equal(Vector2.ZERO)) \ + .is_failed() \ + .has_message(""" + Expecting: + '$v0' + but was + '$v1' + Additional info: + custom failure data""" + .dedent() + .trim_prefix("\n") + .replace("$v0", str(Vector2.ZERO)) + .replace("$v1", str(Vector2.ONE))) + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_vector(null).is_null() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func() -> void: assert_vector(RefCounted.new()).is_null()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_vector(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() diff --git a/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd.uid b/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd.uid new file mode 100644 index 0000000..424f878 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd.uid @@ -0,0 +1 @@ +uid://br1nqmam3dcbt diff --git a/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd b/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd new file mode 100644 index 0000000..edf4f36 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd @@ -0,0 +1,120 @@ +# GdUnit generated TestSuite +class_name CmdArgumentParserTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdArgumentParser.gd' + +var option_a := CmdOption.new("-a", "some help text a", "some description a") +var option_f := CmdOption.new("-f, --foo", "some help text foo", "some description foo") +var option_b := CmdOption.new("-b, --bar", "-b ", "comand with required argument", TYPE_STRING) +var option_c := CmdOption.new("-c, --calc", "-c [value]", "command with optional argument", TYPE_STRING, true) +var option_x := CmdOption.new("-x", "some help text x", "some description x") + +var _cmd_options :CmdOptions + + +func before() -> void: + # setup command options + _cmd_options = CmdOptions.new([ + option_a, + option_f, + option_b, + option_c, + ], + # advnaced options + [ + option_x, + ]) + + +func test_parse_success() -> void: + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + assert_result(parser.parse([])).is_empty() + # check with godot cmd argumnents before tool argument + assert_result(parser.parse(["-d", "dir/dir/CmdTool.gd"])).is_empty() + + # if valid argument set than don't show the help by default + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + ]) + + +func test_parse_success_required_arg() -> void: + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-a", "-b", "valueA", "-b", "valueB"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + CmdCommand.new("-b", ["valueA", "valueB"]), + ]) + + # useing command long term + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-a", "--bar", "value"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + CmdCommand.new("-b", ["value"]) + ]) + + +func test_parse_success_optional_arg() -> void: + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + # without argument + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c"), + CmdCommand.new("-a") + ]) + + # without argument at end + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-a", "-c"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + CmdCommand.new("-c") + ]) + + # with argument + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "argument", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c", ["argument"]), + CmdCommand.new("-a") + ]) + + +func test_parse_success_repead_cmd_args() -> void: + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + # without argument + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "argument", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c", ["argument"]), + CmdCommand.new("-a") + ]) + + # with repeading commands argument + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "argument1", "-a", "-c", "argument2", "-c", "argument3"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c", ["argument1", "argument2", "argument3"]), + CmdCommand.new("-a") + ]) + + +func test_parse_error() -> void: + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + assert_result(parser.parse([])).is_empty() + + # if invalid arguemens set than return with error and show the help by default + assert_result(parser.parse(["-d", "dir/dir/CmdTool.gd", "-unknown"])).is_error()\ + .contains_message("Unknown '-unknown' command!") diff --git a/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd.uid b/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd.uid new file mode 100644 index 0000000..7f00d57 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd.uid @@ -0,0 +1 @@ +uid://m5yxlu0njeek diff --git a/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd b/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd new file mode 100644 index 0000000..002b4ce --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd @@ -0,0 +1,162 @@ +@warning_ignore_start("unsafe_method_access") +# GdUnit generated TestSuite +class_name CmdCommandHandlerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdCommandHandler.gd' + +var _cmd_options: CmdOptions +var _cmd_instance: TestCommands + + +# small example of command class +class TestCommands: + func cmd_no_arg() -> String: + return "" + + func cmd_foo() -> String: + return "cmd_foo" + + func cmd_arg(value: String) -> String: + return value + + func cmd_args(values: PackedStringArray) -> Array: + return values + + func cmd_x() -> String: + return "cmd_x" + + +func before() -> void: + # setup command options + _cmd_options = CmdOptions.new([ + CmdOption.new("-a", "some help text a", "some description a"), + CmdOption.new("-f, --foo", "some help text foo", "some description foo"), + CmdOption.new("-arg, --arg", "some help text bar", "some description bar") + ], + # advnaced options + [ + CmdOption.new("-x", "some help text x", "some description x"), + ]) + _cmd_instance = TestCommands.new() + + +func test_register_cb() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + + # register a single argumend cb + cmd_handler.register_cb("-a", _cmd_instance.cmd_arg) + assert_dict(cmd_handler._command_cbs).contains_key_value("-a", [_cmd_instance.cmd_arg, CmdCommandHandler.NO_CB]) + + +func test_register_invalid_cb() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + # try to register a multi argumend cb + cmd_handler.register_cb("-a", _cmd_instance.cmd_args) + # verify the cb is not registered + assert_dict(cmd_handler._command_cbs).is_empty() + + +func test_register_cbv() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + + # register a single argumend cb + cmd_handler.register_cbv("-a", _cmd_instance.cmd_args) + assert_dict(cmd_handler._command_cbs).contains_key_value("-a", [CmdCommandHandler.NO_CB, _cmd_instance.cmd_args]) + + +func test_register_invalid_cbv() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + # try to register a single argumend cb + cmd_handler.register_cbv("-a", _cmd_instance.cmd_arg) + # verify the cb is not registered + assert_dict(cmd_handler._command_cbs).is_empty() + + +func test_register_cb_and_cbv() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + + cmd_handler.register_cb("-a", _cmd_instance.cmd_arg) + cmd_handler.register_cbv("-a", _cmd_instance.cmd_args) + assert_dict(cmd_handler._command_cbs).contains_key_value("-a", [_cmd_instance.cmd_arg, _cmd_instance.cmd_args]) + + +func test__validate_no_registerd_commands() -> void: + var cmd_handler := CmdCommandHandler.new(_cmd_options) + + assert_result(cmd_handler._validate()).is_success() + + +func test__validate_registerd_commands() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", _cmd_instance.cmd_no_arg) + cmd_handler.register_cb("-f", _cmd_instance.cmd_foo) + cmd_handler.register_cb("-arg", _cmd_instance.cmd_arg) + + assert_result(cmd_handler._validate()).is_success() + + +func test__validate_registerd_unknown_commands() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", _cmd_instance.cmd_no_arg) + cmd_handler.register_cb("-d", _cmd_instance.cmd_foo) + cmd_handler.register_cb("-arg", _cmd_instance.cmd_arg) + cmd_handler.register_cb("-y", _cmd_instance.cmd_x) + + assert_result(cmd_handler._validate())\ + .is_error()\ + .contains_message("The command '-d' is unknown, verify your CmdOptions!\nThe command '-y' is unknown, verify your CmdOptions!") + + +func test__validate_registerd_invalid_callbacks() -> void: + var cmd_handler := CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", _cmd_instance.cmd_no_arg) + cmd_handler.register_cb("-arg", Callable(_cmd_instance, "cmd_not_exists")) + + assert_result(cmd_handler._validate())\ + .is_error()\ + .contains_message("Invalid function reference for command '-arg', Check the function reference!") + + +func test__validate_registerd_register_same_callback_twice() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", _cmd_instance.cmd_no_arg) + cmd_handler.register_cb("-arg", _cmd_instance.cmd_no_arg) + assert_result(cmd_handler._validate())\ + .is_error()\ + .contains_message("The function reference 'cmd_no_arg' already registerd for command '-a'!") + + +func test_execute_no_commands() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + assert_result(cmd_handler.execute([])).is_success() + + +func test_execute_commands_no_cb_registered() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + assert_result(cmd_handler.execute([CmdCommand.new("-a")])).is_success() + + +func test_execute_commands_with_cb_registered() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + var cmd_spy: TestCommands = spy(_cmd_instance) + + cmd_handler.register_cb("-arg", cmd_spy.cmd_arg) + cmd_handler.register_cbv("-arg", cmd_spy.cmd_args) + cmd_handler.register_cb("-a", cmd_spy.cmd_no_arg) + + assert_result(cmd_handler.execute([CmdCommand.new("-a")])).is_success() + + verify(cmd_spy).cmd_no_arg() + verify_no_more_interactions(cmd_spy) + + reset(cmd_spy) + assert_result(cmd_handler.execute([ + CmdCommand.new("-a"), + CmdCommand.new("-arg", ["some_value"]), + CmdCommand.new("-arg", ["value1", "value2"])])).is_success() + verify(cmd_spy).cmd_no_arg() + verify(cmd_spy).cmd_arg("some_value") + verify(cmd_spy).cmd_args(PackedStringArray(["value1", "value2"])) + verify_no_more_interactions(cmd_spy) diff --git a/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd.uid b/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd.uid new file mode 100644 index 0000000..3392495 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd.uid @@ -0,0 +1 @@ +uid://cj5evamgcg580 diff --git a/addons/gdUnit4/test/cmd/CmdCommandTest.gd b/addons/gdUnit4/test/cmd/CmdCommandTest.gd new file mode 100644 index 0000000..30e77f6 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdCommandTest.gd @@ -0,0 +1,32 @@ +# GdUnit generated TestSuite +class_name CmdCommandTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdCommand.gd' + + +func test_create() -> void: + var cmd_a := CmdCommand.new("cmd_a") + assert_str(cmd_a.name()).is_equal("cmd_a") + assert_array(cmd_a.arguments()).is_empty() + + var cmd_b := CmdCommand.new("cmd_b", ["arg1"]) + assert_str(cmd_b.name()).is_equal("cmd_b") + assert_array(cmd_b.arguments()).contains_exactly(["arg1"]) + + assert_object(cmd_a).is_not_equal(cmd_b) + + +func test_add_argument() -> void: + var cmd_a := CmdCommand.new("cmd_a") + cmd_a.add_argument("arg1") + cmd_a.add_argument("arg2") + assert_str(cmd_a.name()).is_equal("cmd_a") + assert_array(cmd_a.arguments()).contains_exactly(["arg1", "arg2"]) + + var cmd_b := CmdCommand.new("cmd_b", ["arg1"]) + cmd_b.add_argument("arg2") + cmd_b.add_argument("arg3") + assert_str(cmd_b.name()).is_equal("cmd_b") + assert_array(cmd_b.arguments()).contains_exactly(["arg1", "arg2", "arg3"]) diff --git a/addons/gdUnit4/test/cmd/CmdCommandTest.gd.uid b/addons/gdUnit4/test/cmd/CmdCommandTest.gd.uid new file mode 100644 index 0000000..dedd175 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdCommandTest.gd.uid @@ -0,0 +1 @@ +uid://ckbtn03rqia0m diff --git a/addons/gdUnit4/test/cmd/CmdOptionTest.gd b/addons/gdUnit4/test/cmd/CmdOptionTest.gd new file mode 100644 index 0000000..ebcb71f --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdOptionTest.gd @@ -0,0 +1,51 @@ +# GdUnit generated TestSuite +class_name CmdOptionTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdOption.gd' + + +func test_commands() -> void: + assert_array(CmdOption.new("-a", "help a", "describe a").commands())\ + .contains_exactly(["-a"]) + assert_array(CmdOption.new("-a, --aaa", "help a", "describe a").commands())\ + .contains_exactly(["-a", "--aaa"]) + # containing space or tabs + assert_array(CmdOption.new("-b , --bb ", "help a", "describe a")\ + .commands()).contains_exactly(["-b", "--bb"]) + + +func test_short_command() -> void: + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").short_command()).is_equal("-a") + + +func test_help() -> void: + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").help()).is_equal("help a") + + +func test_description() -> void: + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").description()).is_equal("describe a") + + +func test_type() -> void: + assert_int(CmdOption.new("-a", "", "").type()).is_equal(TYPE_NIL) + assert_int(CmdOption.new("-a", "", "", TYPE_STRING).type()).is_equal(TYPE_STRING) + assert_int(CmdOption.new("-a", "", "", TYPE_BOOL).type()).is_equal(TYPE_BOOL) + + +func test_is_argument_optional() -> void: + assert_bool(CmdOption.new("-a", "", "").is_argument_optional()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_BOOL, false).is_argument_optional()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_BOOL, true).is_argument_optional()).is_true() + + +func test_has_argument() -> void: + assert_bool(CmdOption.new("-a", "", "").has_argument()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_NIL).has_argument()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_BOOL).has_argument()).is_true() + + +func test_describe() -> void: + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").describe())\ + .is_equal(' ["-a", "--aaa"] describe a \n help a\n') diff --git a/addons/gdUnit4/test/cmd/CmdOptionTest.gd.uid b/addons/gdUnit4/test/cmd/CmdOptionTest.gd.uid new file mode 100644 index 0000000..b8e1851 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdOptionTest.gd.uid @@ -0,0 +1 @@ +uid://b8uq7eu463rdd diff --git a/addons/gdUnit4/test/cmd/CmdOptionsTest.gd b/addons/gdUnit4/test/cmd/CmdOptionsTest.gd new file mode 100644 index 0000000..1151db8 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdOptionsTest.gd @@ -0,0 +1,57 @@ +# GdUnit generated TestSuite +class_name CmdOptionsTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdOptions.gd' + + +var option_a := CmdOption.new("-a", "some help text a", "some description a") +var option_f := CmdOption.new("-f, --foo", "some help text foo", "some description foo") +var option_b := CmdOption.new("-b, --bar", "some help text bar", "some description bar") +var option_x := CmdOption.new("-x", "some help text x", "some description x") + +var _cmd_options :CmdOptions + + +func before() -> void: + # setup command options + _cmd_options = CmdOptions.new([ + option_a, + option_f, + option_b, + ], + # advnaced options + [ + option_x, + ]) + + +func test_get_option() -> void: + assert_object(_cmd_options.get_option("-a")).is_same(option_a) + assert_object(_cmd_options.get_option("-f")).is_same(option_f) + assert_object(_cmd_options.get_option("--foo")).is_same(option_f) + assert_object(_cmd_options.get_option("-b")).is_same(option_b) + assert_object(_cmd_options.get_option("--bar")).is_same(option_b) + assert_object(_cmd_options.get_option("-x")).is_same(option_x) + # for not existsing command + assert_object(_cmd_options.get_option("-z")).is_null() + + +func test_default_options() -> void: + assert_array(_cmd_options.default_options()).contains_exactly([ + option_a, + option_f, + option_b]) + + +func test_advanced_options() -> void: + assert_array(_cmd_options.advanced_options()).contains_exactly([option_x]) + + +func test_options() -> void: + assert_array(_cmd_options.options()).contains_exactly([ + option_a, + option_f, + option_b, + option_x]) diff --git a/addons/gdUnit4/test/cmd/CmdOptionsTest.gd.uid b/addons/gdUnit4/test/cmd/CmdOptionsTest.gd.uid new file mode 100644 index 0000000..98b9bbd --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdOptionsTest.gd.uid @@ -0,0 +1 @@ +uid://dw84c5ujdl8fo diff --git a/addons/gdUnit4/test/cmd/runtestDebug.tscn b/addons/gdUnit4/test/cmd/runtestDebug.tscn new file mode 100644 index 0000000..2a1b40c --- /dev/null +++ b/addons/gdUnit4/test/cmd/runtestDebug.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=2 format=3 uid="uid://cbdmsq72qtcyr"] + +[sub_resource type="GDScript" id="GDScript_54n73"] +script/source = "# a small helper to debug the GdUnitCmdTool +extends Node + +const CmdTool := preload(\"res://addons/gdUnit4/bin/GdUnitCmdTool.gd\") +const CopyLog := preload(\"res://addons/gdUnit4/bin/GdUnitCopyLog.gd\") + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + #var tool := CmdTool.new() + + var runner := GdUnitTestCIRunner.new() + runner._debug_cmd_args = [\"GdUnitCmdTool.gd\", \"--add\", \"res://addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd\", \"--continue\", \"-rc\", \"1\"] + + add_child(runner) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + var copy_log := CopyLog.new() + copy_log._debug_cmd_args = [\"GdUnitCopyLog.gd\"] + copy_log._process(0) + prints(\"exit\") +" + +[node name="RuntestDebug" type="Node"] +script = SubResource("GDScript_54n73") diff --git a/addons/gdUnit4/test/core/GdArrayToolsTest.gd b/addons/gdUnit4/test/core/GdArrayToolsTest.gd new file mode 100644 index 0000000..687bf6d --- /dev/null +++ b/addons/gdUnit4/test/core/GdArrayToolsTest.gd @@ -0,0 +1,182 @@ +# GdUnit generated TestSuite +class_name GdArrayToolsTest +extends GdUnitTestSuite + + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdArrayTools.gd' + + +func test_as_string(_test: String, value: Variant, expected: String, _test_parameters := [ + ['Array', Array([1, 2]), '[1, 2]'], + ['Array', Array([1.0, 2.212]), '[1.000000, 2.212000]'], + ['Array', Array([true, false]), '[true, false]'], + ['Array', Array(["1", "2"]), '["1", "2"]'], + ['Array', Array([Vector2.ZERO, Vector2.LEFT]), '[Vector2(), Vector2'+str(Vector2(-1, 0))+']'], + ['Array', Array([Vector3.ZERO, Vector3.LEFT]), '[Vector3(), Vector3'+str(Vector3(-1, 0, 0))+']'], + ['Array', Array([Color.RED, Color.GREEN]), '[Color'+str(Color(1, 0, 0, 1))+', Color'+str(Color(0, 1, 0, 1))+']'], + ['ArrayInt', Array([1, 2]) as Array[int], '[1, 2]'], + ['ArrayFloat', Array([1.0, 2.212]) as Array[float], '[1.000000, 2.212000]'], + ['ArrayBool', Array([true, false]) as Array[bool], '[true, false]'], + ['ArrayString', Array(["1", "2"]) as Array[String], '["1", "2"]'], + ['ArrayVector2', Array([Vector2.ZERO, Vector2.LEFT]) as Array[Vector2], '[Vector2(), Vector2'+str(Vector2(-1, 0))+']'], + ['ArrayVector2i', Array([Vector2i.ZERO, Vector2i.LEFT]) as Array[Vector2i], '[Vector2i(), Vector2i'+str(Vector2i(-1, 0))+']'], + ['ArrayVector3', Array([Vector3.ZERO, Vector3.LEFT]) as Array[Vector3], '[Vector3(), Vector3'+str(Vector3(-1, 0, 0))+']'], + ['ArrayVector3i', Array([Vector3i.ZERO, Vector3i.LEFT]) as Array[Vector3i], '[Vector3i(), Vector3i'+str(Vector3i(-1, 0, 0))+']'], + ['ArrayVector4', Array([Vector4.ZERO, Vector4.ONE]) as Array[Vector4], '[Vector4(), Vector4%s]' % Vector4(1, 1, 1, 1)], + ['ArrayVector4i', Array([Vector4i.ZERO, Vector4i.ONE]) as Array[Vector4i], '[Vector4i(), Vector4i(1, 1, 1, 1)]'], + ['ArrayColor', Array([Color.RED, Color.GREEN]) as Array[Color], '[Color'+str(Color(1, 0, 0, 1))+', Color'+str(Color(0, 1, 0, 1))+']'], + ['PackedByteArray', PackedByteArray([1, 2]), 'PackedByteArray[1, 2]'], + ['PackedInt32Array', PackedInt32Array([1, 2]), 'PackedInt32Array[1, 2]'], + ['PackedInt64Array', PackedInt64Array([1, 2]), 'PackedInt64Array[1, 2]'], + ['PackedFloat32Array', PackedFloat32Array([1, 2.212]), 'PackedFloat32Array[1.000000, 2.212000]'], + ['PackedFloat64Array', PackedFloat64Array([1, 2.212]), 'PackedFloat64Array[1.000000, 2.212000]'], + ['PackedStringArray', PackedStringArray([1, 2]), 'PackedStringArray["1", "2"]'], + ['PackedVector2Array', PackedVector2Array([Vector2.ZERO, Vector2.LEFT]), 'PackedVector2Array[Vector2(), Vector2'+str(Vector2(-1, 0))+']'], + ['PackedVector3Array', PackedVector3Array([Vector3.ZERO, Vector3.LEFT]), 'PackedVector3Array[Vector3(), Vector3'+str(Vector3(-1, 0, 0))+']'], + ['PackedColorArray', PackedColorArray([Color.RED, Color.GREEN]), 'PackedColorArray[Color'+str(Color(1, 0, 0, 1))+', Color'+str(Color(0, 1, 0, 1))+']'], +]) -> void: + + assert_that(GdArrayTools.as_string(value)).is_equal(expected) + + +func test_as_string_simple_format() -> void: + var value := PackedStringArray(["a", "b"]) + + assert_that(GdArrayTools.as_string(value, false)).is_equal('[a, b]') + + +func test_is_array_type(_test: String, value: Variant, expected: bool, _test_parameters := [ + ['bool', true, false], + ['int', 42, false], + ['float', 1.21, false], + ['String', "abc", false], + ['Dictionary', {}, false], + ['RefCounted', RefCounted.new(), false], + ['Array', Array([1, 2]), true], + ['Array', Array([1.0, 2.212]), true], + ['Array', Array([true, false]), true], + ['Array', Array(["1", "2"]), true], + ['Array', Array([Vector2.ZERO, Vector2.LEFT]), true], + ['Array', Array([Vector3.ZERO, Vector3.LEFT]), true], + ['Array', Array([Color.RED, Color.GREEN]), true], + ['ArrayInt', Array([1, 2]) as Array[int], true], + ['ArrayFloat', Array([1.0, 2.212]) as Array[float], true], + ['ArrayBool', Array([true, false]) as Array[bool], true], + ['ArrayString', Array(["1", "2"]) as Array[String], true], + ['ArrayVector2', Array([Vector2.ZERO, Vector2.LEFT]) as Array[Vector2], true], + ['ArrayVector2i', Array([Vector2i.ZERO, Vector2i.LEFT]) as Array[Vector2i], true], + ['ArrayVector3', Array([Vector3.ZERO, Vector3.LEFT]) as Array[Vector3], true], + ['ArrayVector3i', Array([Vector3i.ZERO, Vector3i.LEFT]) as Array[Vector3i], true], + ['ArrayVector4', Array([Vector4.ZERO, Vector4.ONE]) as Array[Vector4], true], + ['ArrayVector4i', Array([Vector4i.ZERO, Vector4i.ONE]) as Array[Vector4i], true], + ['ArrayColor', Array([Color.RED, Color.GREEN]) as Array[Color], true], + ['PackedByteArray', PackedByteArray([1, 2]), true], + ['PackedInt32Array', PackedInt32Array([1, 2]), true], + ['PackedInt64Array', PackedInt64Array([1, 2]), true], + ['PackedFloat32Array', PackedFloat32Array([1, 2.212]), true], + ['PackedFloat64Array', PackedFloat64Array([1, 2.212]), true], + ['PackedStringArray', PackedStringArray([1, 2]), true], + ['PackedVector2Array', PackedVector2Array([Vector2.ZERO, Vector2.LEFT]), true], + ['PackedVector3Array', PackedVector3Array([Vector3.ZERO, Vector3.LEFT]), true], + ['PackedColorArray', PackedColorArray([Color.RED, Color.GREEN]), true], +]) -> void: + + assert_that(GdArrayTools.is_array_type(value)).is_equal(expected) + + +func test_is_type_array() -> void: + for type :int in [TYPE_NIL, TYPE_MAX]: + if type in [TYPE_ARRAY, TYPE_PACKED_COLOR_ARRAY]: + assert_bool(GdArrayTools.is_type_array(type)).is_true() + else: + assert_bool(GdArrayTools.is_type_array(type)).is_false() + + +func test_filter_value(value: Variant, expected_type: int, _test_parameters := [ + [[1, 2, 3, 1], TYPE_ARRAY], + [Array([1, 2, 3, 1]) as Array[int], TYPE_ARRAY], + [PackedByteArray([1, 2, 3, 1]), TYPE_PACKED_BYTE_ARRAY], + [PackedInt32Array([1, 2, 3, 1]), TYPE_PACKED_INT32_ARRAY], + [PackedInt64Array([1, 2, 3, 1]), TYPE_PACKED_INT64_ARRAY], + [PackedFloat32Array([1.0, 2, 1.1, 1.0]), TYPE_PACKED_FLOAT32_ARRAY], + [PackedFloat64Array([1.0, 2, 1.1, 1.0]), TYPE_PACKED_FLOAT64_ARRAY], + [PackedStringArray(["1", "2", "3", "1"]), TYPE_PACKED_STRING_ARRAY], + [PackedVector2Array([Vector2.ZERO, Vector2.ONE, Vector2.DOWN, Vector2.ZERO]), TYPE_PACKED_VECTOR2_ARRAY], + [PackedVector3Array([Vector3.ZERO, Vector3.ONE, Vector3.DOWN, Vector3.ZERO]), TYPE_PACKED_VECTOR3_ARRAY], + [PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.RED]), TYPE_PACKED_COLOR_ARRAY] + ]) -> void: + + var value_to_remove :Variant = value[0] + var result :Variant = GdArrayTools.filter_value(value, value_to_remove) + assert_array(result).not_contains([value_to_remove]).has_size(2) + assert_that(typeof(result)).is_equal(expected_type) + + +func test_filter_value_() -> void: + assert_array(GdArrayTools.filter_value([], null)).is_empty() + assert_array(GdArrayTools.filter_value([], "")).is_empty() + + var current :Array = [null, "a", "b", null, "c", null] + var filtered :Variant= GdArrayTools.filter_value(current, null) + assert_array(filtered).contains_exactly(["a", "b", "c"]) + # verify the source is not affected + assert_array(current).contains_exactly([null, "a", "b", null, "c", null]) + + current = [null, "a", "xxx", null, "xx", null] + filtered = GdArrayTools.filter_value(current, "xxx") + assert_array(filtered).contains_exactly([null, "a", null, "xx", null]) + # verify the source is not affected + assert_array(current).contains_exactly([null, "a", "xxx", null, "xx", null]) + + +func test_erase_value() -> void: + var current := [] + GdArrayTools.erase_value(current, null) + assert_array(current).is_empty() + + current = [null] + GdArrayTools.erase_value(current, null) + assert_array(current).is_empty() + + current = [null, "a", "b", null, "c", null] + GdArrayTools.erase_value(current, null) + # verify the source is affected + assert_array(current).contains_exactly(["a", "b", "c"]) + + +func test_scan_typed() -> void: + assert_that(GdArrayTools.scan_typed([1, 2, 3])).is_equal(TYPE_INT) + assert_that(GdArrayTools.scan_typed([1, 2.2, 3])).is_equal(GdObjects.TYPE_VARIANT) + + +class ExampleItem: + var _name: String + var _type: int + + func _init(name: String, type: int) -> void: + _name = name + _type = type + + +func test_group_by() -> void: + var values := [ + ExampleItem.new("foo1", 0), + ExampleItem.new("foo2", 0), + ExampleItem.new("bar1", 1), + ExampleItem.new("bar2", 1), + ExampleItem.new("foo3", 0), + ExampleItem.new("foo3", 1), + ExampleItem.new("xxx", 2), + ] + + # We group by type + var result := GdArrayTools.group_by(values, func(item: ExampleItem) -> int: + return item._type + ) + + # Verify grouping result + assert_dict(result).has_size(3)\ + .contains_key_value(0, [values[0], values[1], values[4]])\ + .contains_key_value(1, [values[2], values[3], values[5]])\ + .contains_key_value(2, [values[6]])\ diff --git a/addons/gdUnit4/test/core/GdArrayToolsTest.gd.uid b/addons/gdUnit4/test/core/GdArrayToolsTest.gd.uid new file mode 100644 index 0000000..a5eff08 --- /dev/null +++ b/addons/gdUnit4/test/core/GdArrayToolsTest.gd.uid @@ -0,0 +1 @@ +uid://buguh2a415bnu diff --git a/addons/gdUnit4/test/core/GdDiffToolTest.gd b/addons/gdUnit4/test/core/GdDiffToolTest.gd new file mode 100644 index 0000000..fafa1a8 --- /dev/null +++ b/addons/gdUnit4/test/core/GdDiffToolTest.gd @@ -0,0 +1,48 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdDiffToolTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdDiffTool.gd' + + +func test_string_diff_empty() -> void: + var diffs := GdDiffTool.string_diff("", "") + assert_array(diffs).has_size(2) + assert_array(diffs[0]).is_empty() + assert_array(diffs[1]).is_empty() + + +func test_string_diff_equals() -> void: + var diffs := GdDiffTool.string_diff("Abc", "Abc") + var expected_l_diff := "Abc".to_utf8_buffer() + var expected_r_diff := "Abc".to_utf8_buffer() + + assert_array(diffs).has_size(2) + assert_array(diffs[0]).contains_exactly(expected_l_diff) + assert_array(diffs[1]).contains_exactly(expected_r_diff) + + +func test_string_diff() -> void: + # tests the result of string diff function like assert_str("Abc").is_equal("abc") + var diffs := GdDiffTool.string_diff("Abc", "abc") + var chars := "Aabc".to_utf8_buffer() + var ord_A := chars[0] + var ord_a := chars[1] + var ord_b := chars[2] + var ord_c := chars[3] + var expected_l_diff := PackedByteArray([GdDiffTool.DIV_SUB, ord_A, GdDiffTool.DIV_ADD, ord_a, ord_b, ord_c]) + var expected_r_diff := PackedByteArray([GdDiffTool.DIV_ADD, ord_A, GdDiffTool.DIV_SUB, ord_a, ord_b, ord_c]) + + assert_array(diffs).has_size(2) + assert_array(diffs[0]).contains_exactly(expected_l_diff) + assert_array(diffs[1]).contains_exactly(expected_r_diff) + + +@warning_ignore("unused_parameter") +func test_string_diff_large_value(fuzzer := Fuzzers.rand_str(1000, 4000), fuzzer_iterations := 10) -> void: + # test diff with large values not crashes the API GD-100 + var value :String = fuzzer.next_value() + GdDiffTool.string_diff(value, value) diff --git a/addons/gdUnit4/test/core/GdDiffToolTest.gd.uid b/addons/gdUnit4/test/core/GdDiffToolTest.gd.uid new file mode 100644 index 0000000..733dcef --- /dev/null +++ b/addons/gdUnit4/test/core/GdDiffToolTest.gd.uid @@ -0,0 +1 @@ +uid://be440ro3kfk23 diff --git a/addons/gdUnit4/test/core/GdObjectsTest.gd b/addons/gdUnit4/test/core/GdObjectsTest.gd new file mode 100644 index 0000000..844aeca --- /dev/null +++ b/addons/gdUnit4/test/core/GdObjectsTest.gd @@ -0,0 +1,528 @@ +extends GdUnitTestSuite + + +func test_equals_string() -> void: + var a := "" + var b := "" + var c := "abc" + var d := "abC" + + assert_bool(GdObjects.equals("", "")).is_true() + assert_bool(GdObjects.equals(a, "")).is_true() + assert_bool(GdObjects.equals("", a)).is_true() + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(c, c)).is_true() + assert_bool(GdObjects.equals(c, String(c))).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals("", c)).is_false() + assert_bool(GdObjects.equals(c, "")).is_false() + assert_bool(GdObjects.equals(c, d)).is_false() + assert_bool(GdObjects.equals(d, c)).is_false() + # against diverent type + assert_bool(GdObjects.equals(d, Array())).is_false() + assert_bool(GdObjects.equals(d, Dictionary())).is_false() + assert_bool(GdObjects.equals(d, Vector2.ONE)).is_false() + assert_bool(GdObjects.equals(d, Vector3.ONE)).is_false() + + +func test_equals_stringname() -> void: + assert_bool(GdObjects.equals("", &"")).is_true() + assert_bool(GdObjects.equals("abc", &"abc")).is_true() + assert_bool(GdObjects.equals("abc", &"abC")).is_false() + + +func test_equals_array() -> void: + var a := [] + var b := [] + var c := Array() + var d := [1,2,3,4,5] + var e := [1,2,3,4,5] + var x := [1,2,3,6,4,5] + + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(a, c)).is_true() + assert_bool(GdObjects.equals(c, b)).is_true() + assert_bool(GdObjects.equals(d, d)).is_true() + assert_bool(GdObjects.equals(d, e)).is_true() + assert_bool(GdObjects.equals(e, d)).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals(a, d)).is_false() + assert_bool(GdObjects.equals(d, a)).is_false() + assert_bool(GdObjects.equals(d, x)).is_false() + assert_bool(GdObjects.equals(x, d)).is_false() + # against diverent type + assert_bool(GdObjects.equals(a, "")).is_false() + assert_bool(GdObjects.equals(a, Dictionary())).is_false() + assert_bool(GdObjects.equals(a, Vector2.ONE)).is_false() + assert_bool(GdObjects.equals(a, Vector3.ONE)).is_false() + + +func test_equals_dictionary() -> void: + var a := {} + var b := {} + var c := {"a":"foo"} + var d := {"a":"foo"} + var e1 := {"a":"foo", "b":"bar"} + var e2 := {"b":"bar", "a":"foo"} + + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(c, c)).is_true() + assert_bool(GdObjects.equals(c, d)).is_true() + assert_bool(GdObjects.equals(e1, e2)).is_true() + assert_bool(GdObjects.equals(e2, e1)).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals(a, c)).is_false() + assert_bool(GdObjects.equals(c, a)).is_false() + assert_bool(GdObjects.equals(a, e1)).is_false() + assert_bool(GdObjects.equals(e1, a)).is_false() + assert_bool(GdObjects.equals(c, e1)).is_false() + assert_bool(GdObjects.equals(e1, c)).is_false() + + +class TestClass extends Resource: + + enum { + A, + B + } + + var _a:int + var _b:String + var _c:Array + + func _init(a:int = 0, b:String = "", c:Array = []) -> void: + _a = a + _b = b + _c = c + + +func test_equals_class() -> void: + var a := TestClass.new() + var b := TestClass.new() + var c := TestClass.new(1, "foo", ["bar", "xxx"]) + var d := TestClass.new(1, "foo", ["bar", "xxx"]) + var x := TestClass.new(1, "foo", ["bar", "xsxx"]) + + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(c, d)).is_true() + assert_bool(GdObjects.equals(d, c)).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals(a, c)).is_false() + assert_bool(GdObjects.equals(c, a)).is_false() + assert_bool(GdObjects.equals(d, x)).is_false() + assert_bool(GdObjects.equals(x, d)).is_false() + + +func test_equals_with_stack_deep() -> void: + # more extended version + var x2 := TestClass.new(1, "foo", [TestClass.new(22, "foo"), TestClass.new(22, "foo")]) + var x3 := TestClass.new(1, "foo", [TestClass.new(22, "foo"), TestClass.new(23, "foo")]) + assert_bool(GdObjects.equals(x2, x3)).is_false() + + +func test_equals_Node_with_deep_check() -> void: + var nodeA :Node = auto_free(Node.new()) + var nodeB :Node = auto_free(Node.new()) + + # compares by default with deep parameter ckeck + assert_bool(GdObjects.equals(nodeA, nodeA)).is_true() + assert_bool(GdObjects.equals(nodeB, nodeB)).is_true() + assert_bool(GdObjects.equals(nodeA, nodeB)).is_true() + assert_bool(GdObjects.equals(nodeB, nodeA)).is_true() + # compares by object reference + assert_bool(GdObjects.equals(nodeA, nodeA, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_true() + assert_bool(GdObjects.equals(nodeB, nodeB, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_true() + assert_bool(GdObjects.equals(nodeA, nodeB, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_false() + assert_bool(GdObjects.equals(nodeB, nodeA, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_false() + + +func test_is_primitive_type() -> void: + assert_bool(GdObjects.is_primitive_type(false)).is_true() + assert_bool(GdObjects.is_primitive_type(true)).is_true() + assert_bool(GdObjects.is_primitive_type(0)).is_true() + assert_bool(GdObjects.is_primitive_type(0.1)).is_true() + assert_bool(GdObjects.is_primitive_type("")).is_true() + assert_bool(GdObjects.is_primitive_type(Vector2.ONE)).is_false() + + +class TestClassForIsType: + var x :int + + +func test_is_type() -> void: + # check build-in types + assert_bool(GdObjects.is_type(1)).is_false() + assert_bool(GdObjects.is_type(1.3)).is_false() + assert_bool(GdObjects.is_type(true)).is_false() + assert_bool(GdObjects.is_type(false)).is_false() + assert_bool(GdObjects.is_type([])).is_false() + assert_bool(GdObjects.is_type("abc")).is_false() + + assert_bool(GdObjects.is_type(null)).is_false() + # an object type + assert_bool(GdObjects.is_type(Node)).is_true() + # an reference type + assert_bool(GdObjects.is_type(AStar3D)).is_true() + # an script type + assert_bool(GdObjects.is_type(GDScript)).is_true() + # an custom type + assert_bool(GdObjects.is_type(TestClassForIsType)).is_true() + # checked inner class type + assert_bool(GdObjects.is_type(CustomClass.InnerClassA)).is_true() + assert_bool(GdObjects.is_type(CustomClass.InnerClassC)).is_true() + + # for instances must allways endup with false + assert_bool(GdObjects.is_type(auto_free(Node.new()))).is_false() + assert_bool(GdObjects.is_type(AStar3D.new())).is_false() + assert_bool(GdObjects.is_type(Dictionary())).is_false() + assert_bool(GdObjects.is_type(PackedColorArray())).is_false() + assert_bool(GdObjects.is_type(GDScript.new())).is_false() + assert_bool(GdObjects.is_type(TestClassForIsType.new())).is_false() + assert_bool(GdObjects.is_type(auto_free(CustomClass.InnerClassC.new()))).is_false() + + +func test_is_singleton() -> void: + for singleton_name in Engine.get_singleton_list(): + var singleton := Engine.get_singleton(singleton_name) + assert_bool(GdObjects.is_singleton(singleton)) \ + .override_failure_message("Expect to a singleton: '%s' Instance: %s, Class: %s" % [singleton_name, singleton, singleton.get_class()]) \ + .is_true() + # false tests + assert_bool(GdObjects.is_singleton(10)).is_false() + assert_bool(GdObjects.is_singleton(true)).is_false() + assert_bool(GdObjects.is_singleton(Node)).is_false() + assert_bool(GdObjects.is_singleton(auto_free(Node.new()))).is_false() + + +func _is_instance(value :Variant) -> bool: + return GdObjects.is_instance(auto_free(value)) + + +func test_is_instance_true() -> void: + assert_bool(_is_instance(RefCounted.new())).is_true() + assert_bool(_is_instance(Node.new())).is_true() + assert_bool(_is_instance(AStar3D.new())).is_true() + assert_bool(_is_instance(PackedScene.new())).is_true() + assert_bool(_is_instance(GDScript.new())).is_true() + assert_bool(_is_instance(Person.new())).is_true() + assert_bool(_is_instance(CustomClass.new())).is_true() + assert_bool(_is_instance(CustomNodeTestClass.new())).is_true() + assert_bool(_is_instance(TestClassForIsType.new())).is_true() + assert_bool(_is_instance(CustomClass.InnerClassC.new())).is_true() + + +func test_is_instance_false() -> void: + assert_bool(_is_instance(RefCounted)).is_false() + assert_bool(_is_instance(Node)).is_false() + assert_bool(_is_instance(AStar3D)).is_false() + assert_bool(_is_instance(PackedScene)).is_false() + assert_bool(_is_instance(GDScript)).is_false() + assert_bool(_is_instance(Dictionary())).is_false() + assert_bool(_is_instance(PackedColorArray())).is_false() + assert_bool(_is_instance(Person)).is_false() + assert_bool(_is_instance(CustomClass)).is_false() + assert_bool(_is_instance(CustomNodeTestClass)).is_false() + assert_bool(_is_instance(TestClassForIsType)).is_false() + assert_bool(_is_instance(CustomClass.InnerClassC)).is_false() + + +# shorter helper func to extract class name and using auto_free +func extract_class_name(value :Variant) -> GdUnitResult: + return GdObjects.extract_class_name(auto_free(value)) + + +func test_get_class_name_from_class_path() -> void: + # extract class name by resoure path + assert_result(extract_class_name("res://addons/gdUnit4/test/resources/core/Person.gd"))\ + .is_success().is_value("Person") + assert_result(extract_class_name("res://addons/gdUnit4/test/resources/core/CustomClass.gd"))\ + .is_success().is_value("CustomClass") + assert_result(extract_class_name("res://addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd"))\ + .is_success().is_value("CustomNodeTestClass") + assert_result(extract_class_name("res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd"))\ + .is_success().is_value("CustomResourceTestClass") + assert_result(extract_class_name("res://addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd"))\ + .is_success().is_value("OverridenGetClassTestClass") + + +func test_get_class_name_from_snake_case_class_path() -> void: + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd"))\ + .is_success().is_value("SnakeCaseWithClassName") + # without class_name + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd"))\ + .is_success().is_value("SnakeCaseWithoutClassName") + + +func test_get_class_name_from_pascal_case_class_path() -> void: + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd"))\ + .is_success().is_value("PascalCaseWithClassName") + # without class_name + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd"))\ + .is_success().is_value("PascalCaseWithoutClassName") + + +func test_get_class_name_from_type() -> void: + assert_result(extract_class_name(Animation)).is_success().is_value("Animation") + assert_result(extract_class_name(GDScript)).is_success().is_value("GDScript") + assert_result(extract_class_name(Camera3D)).is_success().is_value("Camera3D") + assert_result(extract_class_name(Node)).is_success().is_value("Node") + assert_result(extract_class_name(Tree)).is_success().is_value("Tree") + # extract class name from custom classes + assert_result(extract_class_name(Person)).is_success().is_value("Person") + assert_result(extract_class_name(CustomClass)).is_success().is_value("CustomClass") + assert_result(extract_class_name(CustomNodeTestClass)).is_success().is_value("CustomNodeTestClass") + assert_result(extract_class_name(CustomResourceTestClass)).is_success().is_value("CustomResourceTestClass") + assert_result(extract_class_name(OverridenGetClassTestClass)).is_success().is_value("OverridenGetClassTestClass") + assert_result(extract_class_name(AdvancedTestClass)).is_success().is_value("AdvancedTestClass") + + +func test_get_class_name_from_inner_class() -> void: + assert_result(extract_class_name(CustomClass))\ + .is_success().is_value("CustomClass") + assert_result(extract_class_name(CustomClass.InnerClassA))\ + .is_success().is_value("CustomClass.InnerClassA") + assert_result(extract_class_name(CustomClass.InnerClassB))\ + .is_success().is_value("CustomClass.InnerClassB") + assert_result(extract_class_name(CustomClass.InnerClassC))\ + .is_success().is_value("CustomClass.InnerClassC") + assert_result(extract_class_name(CustomClass.InnerClassD))\ + .is_success().is_value("CustomClass.InnerClassD") + assert_result(extract_class_name(AdvancedTestClass.SoundData))\ + .is_success().is_value("AdvancedTestClass.SoundData") + assert_result(extract_class_name(AdvancedTestClass.AtmosphereData))\ + .is_success().is_value("AdvancedTestClass.AtmosphereData") + assert_result(extract_class_name(AdvancedTestClass.Area4D))\ + .is_success().is_value("AdvancedTestClass.Area4D") + + +func test_extract_class_name_from_instance() -> void: + assert_result(extract_class_name(Camera3D.new())).is_equal("Camera3D") + assert_result(extract_class_name(GDScript.new())).is_equal("GDScript") + assert_result(extract_class_name(Node.new())).is_equal("Node") + + # extract class name from custom classes + assert_result(extract_class_name(Person.new())).is_equal("Person") + assert_result(extract_class_name(ClassWithNameA.new())).is_equal("ClassWithNameA") + assert_result(extract_class_name(ClassWithNameB.new())).is_equal("ClassWithNameB") + var classWithoutNameA: GDScript = load("res://addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd") + assert_result(extract_class_name(classWithoutNameA.new())).is_equal("ClassWithoutNameA") + assert_result(extract_class_name(CustomNodeTestClass.new())).is_equal("CustomNodeTestClass") + assert_result(extract_class_name(CustomResourceTestClass.new())).is_equal("CustomResourceTestClass") + assert_result(extract_class_name(OverridenGetClassTestClass.new())).is_equal("OverridenGetClassTestClass") + assert_result(extract_class_name(AdvancedTestClass.new())).is_equal("AdvancedTestClass") + # extract inner class name + assert_result(extract_class_name(AdvancedTestClass.SoundData.new())).is_equal("AdvancedTestClass.SoundData") + assert_result(extract_class_name(AdvancedTestClass.AtmosphereData.new())).is_equal("AdvancedTestClass.AtmosphereData") + assert_result(extract_class_name(AdvancedTestClass.Area4D.new(0))).is_equal("AdvancedTestClass.Area4D") + assert_result(extract_class_name(CustomClass.InnerClassC.new())).is_equal("CustomClass.InnerClassC") + + +# verify enigne class names are not converted by configured naming convention +@warning_ignore("unused_parameter") +func test_extract_class_name_from_class_path(fuzzer := GodotClassNameFuzzer.new(true, true), fuzzer_iterations := 100) -> void: + var clazz_name :String = fuzzer.next_value() + assert_str(GdObjects.extract_class_name_from_class_path(PackedStringArray([clazz_name]))).is_equal(clazz_name) + + +@warning_ignore("unused_parameter") +func test_extract_class_name_godot_classes(fuzzer := GodotClassNameFuzzer.new(true, true), fuzzer_iterations := 100) -> void: + var extract_class_name_ := fuzzer.next_value() as String + var instance :Variant = ClassDB.instantiate(extract_class_name_) + assert_result(extract_class_name(instance)).is_equal(extract_class_name_) + + +func test_extract_class_path_by_clazz() -> void: + # engine classes has no class path + assert_array(GdObjects.extract_class_path(Animation)).is_empty() + assert_array(GdObjects.extract_class_path(GDScript)).is_empty() + assert_array(GdObjects.extract_class_path(Camera3D)).is_empty() + assert_array(GdObjects.extract_class_path(Tree)).is_empty() + assert_array(GdObjects.extract_class_path(Node)).is_empty() + + # script classes + assert_array(GdObjects.extract_class_path(Person))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/Person.gd"]) + assert_array(GdObjects.extract_class_path(CustomClass))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd"]) + assert_array(GdObjects.extract_class_path(CustomNodeTestClass))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd"]) + assert_array(GdObjects.extract_class_path(CustomResourceTestClass))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd"]) + assert_array(GdObjects.extract_class_path(OverridenGetClassTestClass))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd"]) + + # script inner classes + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassA))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassA"]) + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassB))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassB"]) + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassC))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassC"]) + assert_array(GdObjects.extract_class_path(AdvancedTestClass.SoundData))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd", "SoundData"]) + assert_array(GdObjects.extract_class_path(AdvancedTestClass.AtmosphereData))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd", "AtmosphereData"]) + assert_array(GdObjects.extract_class_path(AdvancedTestClass.Area4D))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd", "Area4D"]) + + # inner inner class + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassD.InnerInnerClassA))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassD", "InnerInnerClassA"]) + + +#func __test_can_instantiate(): +# assert_bool(GdObjects.can_instantiate(GDScript)).is_true() +# assert_bool(GdObjects.can_instantiate(Node)).is_true() +# assert_bool(GdObjects.can_instantiate(Tree)).is_true() +# assert_bool(GdObjects.can_instantiate(Camera3D)).is_true() +# assert_bool(GdObjects.can_instantiate(Person)).is_true() +# assert_bool(GdObjects.can_instantiate(CustomClass.InnerClassA)).is_true() +# assert_bool(GdObjects.can_instantiate(TreeItem)).is_true() +# +# creates a test instance by given class name or resource path +# instances created with auto free +func create_instance(clazz :Variant) -> Object: + var result := GdObjects.create_instance(clazz) + if result.is_success(): + return auto_free(result.value()) + return null + + +func test_create_instance_by_class_name() -> void: + # instance of engine classes + assert_object(create_instance(Node))\ + .is_not_null()\ + .is_instanceof(Node) + assert_object(create_instance(Camera3D))\ + .is_not_null()\ + .is_instanceof(Camera3D) + # instance of custom classes + assert_object(create_instance(Person))\ + .is_not_null()\ + .is_instanceof(Person) + # instance of inner classes + assert_object(create_instance(CustomClass.InnerClassA))\ + .is_not_null()\ + .is_instanceof(CustomClass.InnerClassA) + + +func test_extract_class_name_on_null_value() -> void: + # we can't extract class name from a null value + assert_result(GdObjects.extract_class_name(null))\ + .is_error()\ + .contains_message("Can't extract class name form a null value.") + + +func test_is_public_script_class() -> void: + # snake case format class names + assert_bool(GdObjects.is_public_script_class("ScriptWithClassName")).is_true() + assert_bool(GdObjects.is_public_script_class("script_without_class_name")).is_false() + assert_bool(GdObjects.is_public_script_class("CustomClass")).is_true() + # inner classes not listed as public classes + assert_bool(GdObjects.is_public_script_class("CustomClass.InnerClassA")).is_false() + + +func test_is_instance_scene() -> void: + # checked none scene objects + assert_bool(GdObjects.is_instance_scene(RefCounted.new())).is_false() + assert_bool(GdObjects.is_instance_scene(CustomClass.new())).is_false() + assert_bool(GdObjects.is_instance_scene(auto_free(Control.new()))).is_false() + + # now check checked a loaded scene + var resource: PackedScene = load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_bool(GdObjects.is_instance_scene(resource)).is_false() + # checked a instance of a scene + assert_bool(GdObjects.is_instance_scene(auto_free(resource.instantiate()))).is_true() + + +func test_is_scene_resource_path() -> void: + assert_bool(GdObjects.is_scene_resource_path(RefCounted.new())).is_false() + assert_bool(GdObjects.is_scene_resource_path(CustomClass.new())).is_false() + assert_bool(GdObjects.is_scene_resource_path(auto_free(Control.new()))).is_false() + + # check checked a loaded scene + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_bool(GdObjects.is_scene_resource_path(resource)).is_false() + # checked resource path + assert_bool(GdObjects.is_scene_resource_path("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn")).is_true() + + +func test_extract_class_functions() -> void: + var functions := GdObjects.extract_class_functions("Resource", [""]) + for f :Dictionary in functions: + if f["name"] == "get_path": + assert_str(GdFunctionDescriptor.extract_from(f)._to_string()).is_equal("[Line:-1] func get_path() -> String:") + + functions = GdObjects.extract_class_functions("CustomResourceTestClass", ["res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd"]) + for f :Dictionary in functions: + if f["name"] == "get_path": + assert_str(GdFunctionDescriptor.extract_from(f)._to_string()).is_equal("[Line:-1] func get_path() -> String:") + + +func test_all_types() -> void: + var expected_types :Array[int] = [] + for type_index in TYPE_MAX: + expected_types.append(type_index) + + expected_types.append(GdObjects.TYPE_VOID) + expected_types.append(GdObjects.TYPE_VARARG) + expected_types.append(GdObjects.TYPE_FUNC) + expected_types.append(GdObjects.TYPE_FUZZER) + expected_types.append(GdObjects.TYPE_VARIANT) + assert_array(GdObjects.all_types()).contains_exactly_in_any_order(expected_types) + + +func test_to_camel_case() -> void: + assert_str(GdObjects.to_camel_case("MyClassName")).is_equal("myClassName") + assert_str(GdObjects.to_camel_case("my_class_name")).is_equal("myClassName") + assert_str(GdObjects.to_camel_case("myClassName")).is_equal("myClassName") + + +func test_to_pascal_case() -> void: + assert_str(GdObjects.to_pascal_case("MyClassName")).is_equal("MyClassName") + assert_str(GdObjects.to_pascal_case("my_class_name")).is_equal("MyClassName") + assert_str(GdObjects.to_pascal_case("myClassName")).is_equal("MyClassName") + + +func test_to_snake_case() -> void: + assert_str(GdObjects.to_snake_case("MyClassName")).is_equal("my_class_name") + assert_str(GdObjects.to_snake_case("my_class_name")).is_equal("my_class_name") + assert_str(GdObjects.to_snake_case("myClassName")).is_equal("my_class_name") + + +func test_is_snake_case() -> void: + assert_bool(GdObjects.is_snake_case("my_class_name")).is_true() + assert_bool(GdObjects.is_snake_case("myclassname")).is_true() + assert_bool(GdObjects.is_snake_case("MyClassName")).is_false() + assert_bool(GdObjects.is_snake_case("my_class_nameTest")).is_false() + + +class ObjectWithSceneReferece: + var _node: Node + + func _init(node: Node) -> void: + _node = node + + +func test_is_equal_on_scene_embedded_script() -> void: + @warning_ignore("unsafe_method_access") + var node: Node = auto_free(load("res://addons/gdUnit4/test/core/resources/scenes/SceneWithEmbeddedScript.tscn").instantiate()) + + GdObjects.equals(ObjectWithSceneReferece.new(node), ObjectWithSceneReferece.new(node), false) + assert_object(ObjectWithSceneReferece.new(node)).is_equal(ObjectWithSceneReferece.new(node)) diff --git a/addons/gdUnit4/test/core/GdObjectsTest.gd.uid b/addons/gdUnit4/test/core/GdObjectsTest.gd.uid new file mode 100644 index 0000000..3a70a4d --- /dev/null +++ b/addons/gdUnit4/test/core/GdObjectsTest.gd.uid @@ -0,0 +1 @@ +uid://ce3ym1u85q45a diff --git a/addons/gdUnit4/test/core/GdUnit4VersionTest.gd b/addons/gdUnit4/test/core/GdUnit4VersionTest.gd new file mode 100644 index 0000000..5868c6c --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnit4VersionTest.gd @@ -0,0 +1,71 @@ +# GdUnit generated TestSuite +class_name GdUnit4VersionTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/.gd' + + +func test_parse() -> void: + var expected := GdUnit4Version.new(0, 9, 1) + assert_object(GdUnit4Version.parse("v0.9.1-rc")).is_equal(expected) + assert_object(GdUnit4Version.parse("v0.9.1RC")).is_equal(expected) + assert_object(GdUnit4Version.parse("0.9.1 rc")).is_equal(expected) + assert_object(GdUnit4Version.parse("0.9.1")).is_equal(expected) + + +func test_equals() -> void: + var version := GdUnit4Version.new(0, 9, 1) + assert_bool(version.equals(version)).is_true() + assert_bool(version.equals(GdUnit4Version.new(0, 9, 1))).is_true() + assert_bool(GdUnit4Version.new(0, 9, 1).equals(version)).is_true() + + assert_bool(GdUnit4Version.new(0, 9, 2).equals(version)).is_false() + assert_bool(GdUnit4Version.new(0, 8, 1).equals(version)).is_false() + assert_bool(GdUnit4Version.new(1, 9, 1).equals(version)).is_false() + + +func test_to_string() -> void: + var version := GdUnit4Version.new(0, 9, 1) + assert_str(str(version)).is_equal("v0.9.1") + assert_str("%s" % version).is_equal("v0.9.1") + + +@warning_ignore("unused_parameter") +func test_is_greater_major(fuzzer_major := Fuzzers.rangei(1, 20), fuzzer_minor := Fuzzers.rangei(0, 20), fuzzer_patch := Fuzzers.rangei(0, 20), fuzzer_iterations := 500) -> void: + var version := GdUnit4Version.new(0, 9, 1) + @warning_ignore("unsafe_cast") + var current := GdUnit4Version.new(fuzzer_major.next_value() as int, fuzzer_minor.next_value() as int, fuzzer_patch.next_value() as int); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is greater then %s" % [current, version])\ + .is_true() + + +@warning_ignore("unused_parameter") +func test_is_not_greater_major(fuzzer_major := Fuzzers.rangei(1, 10), fuzzer_minor := Fuzzers.rangei(0, 20), fuzzer_patch := Fuzzers.rangei(0, 20), fuzzer_iterations := 500) -> void: + var version := GdUnit4Version.new(11, 0, 0) + @warning_ignore("unsafe_cast") + var current := GdUnit4Version.new(fuzzer_major.next_value() as int, fuzzer_minor.next_value() as int, fuzzer_patch.next_value() as int); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is not greater then %s" % [current, version])\ + .is_false() + + +@warning_ignore("unused_parameter") +func test_is_greater_minor(fuzzer_minor := Fuzzers.rangei(3, 20), fuzzer_patch := Fuzzers.rangei(0, 20), fuzzer_iterations := 500) -> void: + var version := GdUnit4Version.new(0, 2, 1) + @warning_ignore("unsafe_cast") + var current := GdUnit4Version.new(0, fuzzer_minor.next_value() as int, fuzzer_patch.next_value() as int); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is greater then %s" % [current, version])\ + .is_true() + + +@warning_ignore("unused_parameter") +func test_is_greater_patch(fuzzer_patch := Fuzzers.rangei(1, 20), fuzzer_iterations := 500) -> void: + var version := GdUnit4Version.new(0, 2, 0) + @warning_ignore("unsafe_cast") + var current := GdUnit4Version.new(0, 2, fuzzer_patch.next_value() as int); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is greater then %s" % [current, version])\ + .is_true() diff --git a/addons/gdUnit4/test/core/GdUnit4VersionTest.gd.uid b/addons/gdUnit4/test/core/GdUnit4VersionTest.gd.uid new file mode 100644 index 0000000..cd8980c --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnit4VersionTest.gd.uid @@ -0,0 +1 @@ +uid://d2poq0gjlr8b3 diff --git a/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd b/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd new file mode 100644 index 0000000..b45c324 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd @@ -0,0 +1,233 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitFileAccess.gd' + + +var file_to_save :String + + +func after() -> void: + # verify tmp files are deleted automatically + assert_bool(FileAccess.file_exists(file_to_save)).is_false() + + +func _create_file(p_path :String, p_name :String) -> void: + var file := create_temp_file(p_path, p_name) + file.store_string("some content") + + +func test_copy_directory() -> void: + var temp_dir := create_temp_dir("test_copy_directory") + assert_bool(GdUnitFileAccess.copy_directory("res://addons/gdUnit4/test/core/resources/copy_test/folder_a/", temp_dir)).is_true() + assert_file("%s/file_a.txt" % temp_dir).exists() + assert_file("%s/file_b.txt" % temp_dir).exists() + + +func test_copy_directory_recursive() -> void: + var temp_dir := create_temp_dir("test_copy_directory_recursive") + assert_bool(GdUnitFileAccess.copy_directory("res://addons/gdUnit4/test/core/resources/copy_test/", temp_dir, true)).is_true() + assert_file("%s/folder_a/file_a.txt" % temp_dir).exists() + assert_file("%s/folder_a/file_b.txt" % temp_dir).exists() + assert_file("%s/folder_b/file_a.txt" % temp_dir).exists() + assert_file("%s/folder_b/file_b.txt" % temp_dir).exists() + assert_file("%s/folder_b/folder_ba/file_x.txt" % temp_dir).exists() + assert_file("%s/folder_c/file_z.txt" % temp_dir).exists() + + +func test_create_temp_dir() -> void: + var temp_dir := create_temp_dir("examples/game/save") + file_to_save = temp_dir + "/save_game.dat" + + var data := { + 'user': "Hoschi", + 'level': 42 + } + var file := FileAccess.open(file_to_save, FileAccess.WRITE) + file.store_line(JSON.stringify(data)) + assert_bool(FileAccess.file_exists(file_to_save)).is_true() + + +func test_create_temp_file() -> void: + # setup - stores a tmp file with "user://tmp/examples/game/game.sav" (auto closed) + var file := create_temp_file("examples/game", "game.sav") + assert_object(file).is_not_null() + # write some example data + file.store_line("some data") + file.close() + + # verify + var file_read := create_temp_file("examples/game", "game.sav", FileAccess.READ) + assert_object(file_read).is_not_null() + assert_str(file_read.get_as_text()).is_equal("some data\n") + # not needs to be manually close, will be auto closed after test suite execution + + +func test_make_qualified_path() -> void: + assert_str(GdUnitFileAccess.make_qualified_path("MyTest")).is_equal("MyTest") + assert_str(GdUnitFileAccess.make_qualified_path("/MyTest.gd")).is_equal("res://MyTest.gd") + assert_str(GdUnitFileAccess.make_qualified_path("/foo/bar/MyTest.gd")).is_equal("res://foo/bar/MyTest.gd") + assert_str(GdUnitFileAccess.make_qualified_path("res://MyTest.gd")).is_equal("res://MyTest.gd") + assert_str(GdUnitFileAccess.make_qualified_path("res://foo/bar/MyTest.gd")).is_equal("res://foo/bar/MyTest.gd") + + +func test_find_last_path_index() -> void: + # not existing directory + assert_int(GdUnitFileAccess.find_last_path_index("/foo", "report_")).is_equal(0) + # empty directory + var temp_dir := create_temp_dir("test_reports") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(0) + # create some report directories + create_temp_dir("test_reports/report_1") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(1) + create_temp_dir("test_reports/report_2") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(2) + create_temp_dir("test_reports/report_3") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(3) + create_temp_dir("test_reports/report_5") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(5) + # create some more + for index in range(10, 42): + create_temp_dir("test_reports/report_%d" % index) + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(41) + + +func test_delete_path_index_lower_equals_than() -> void: + var temp_dir := create_temp_dir("test_reports_delete") + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).is_empty() + assert_int(GdUnitFileAccess.delete_path_index_lower_equals_than(temp_dir, "report_", 0)).is_equal(0) + + # create some directories + for index in range(10, 42): + create_temp_dir("test_reports_delete/report_%d" % index) + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).has_size(32) + + # try to delete directories with index lower than 0, shold delete nothing + assert_int(GdUnitFileAccess.delete_path_index_lower_equals_than(temp_dir, "report_", 0)).is_equal(0) + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).has_size(32) + + # try to delete directories with index lower_equals than 30 + # shold delet directories report_10 to report_30 = 21 + assert_int(GdUnitFileAccess.delete_path_index_lower_equals_than(temp_dir, "report_", 30)).is_equal(21) + # and 12 directories are left + assert_array(GdUnitFileAccess.scan_dir(temp_dir))\ + .has_size(11)\ + .contains([ + "report_31", + "report_32", + "report_33", + "report_34", + "report_35", + "report_36", + "report_37", + "report_38", + "report_39", + "report_40", + "report_41", + ]) + + +func test_scan_dir() -> void: + var temp_dir := create_temp_dir("test_scan_dir") + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).is_empty() + + create_temp_dir("test_scan_dir/report_2") + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).contains_exactly(["report_2"]) + # create some more directories and files + create_temp_dir("test_scan_dir/report_4") + create_temp_dir("test_scan_dir/report_5") + create_temp_dir("test_scan_dir/report_6") + create_temp_file("test_scan_dir", "file_a") + create_temp_file("test_scan_dir", "file_b") + # this shoul not be counted it is a file in a subdirectory + create_temp_file("test_scan_dir/report_6", "file_b") + assert_array(GdUnitFileAccess.scan_dir(temp_dir))\ + .has_size(6)\ + .contains([ + "report_2", + "report_4", + "report_5", + "report_6", + "file_a", + "file_b"]) + + +func test_delete_directory() -> void: + var tmp_dir := create_temp_dir("test_delete_dir") + create_temp_dir("test_delete_dir/data1") + create_temp_dir("test_delete_dir/data2") + _create_file("test_delete_dir", "example_a.txt") + _create_file("test_delete_dir", "example_b.txt") + _create_file("test_delete_dir", ".hidden_file.txt") + _create_file("test_delete_dir/data1", "example.txt") + _create_file("test_delete_dir/data2", "example2.txt") + + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).contains_exactly_in_any_order([ + "data1", + "data2", + "example_a.txt", + "example_b.txt", + ".hidden_file.txt" + ]) + + # Delete the entire directory and its contents + GdUnitFileAccess.delete_directory(tmp_dir) + assert_bool(DirAccess.dir_exists_absolute(tmp_dir)).is_false() + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).is_empty() + + +func test_delete_directory_content_only() -> void: + var tmp_dir := create_temp_dir("test_delete_dir") + create_temp_dir("test_delete_dir/data1") + create_temp_dir("test_delete_dir/data2") + _create_file("test_delete_dir", "example_a.txt") + _create_file("test_delete_dir", "example_b.txt") + _create_file("test_delete_dir/data1", "example.txt") + _create_file("test_delete_dir/data2", "example2.txt") + + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).contains_exactly_in_any_order([ + "data1", + "data2", + "example_a.txt", + "example_b.txt" + ]) + + # Delete the entire directory and its contents + GdUnitFileAccess.delete_directory(tmp_dir, true) + assert_bool(DirAccess.dir_exists_absolute(tmp_dir)).is_true() + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).is_empty() + + +func test_extract_package() -> void: + clean_temp_dir() + var tmp_path := GdUnitFileAccess.create_temp_dir("test_update") + var source := "res://addons/gdUnit4/test/update/resources/update.zip" + + # the temp should be inital empty + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).is_empty() + # now extract to temp + var result := GdUnitFileAccess.extract_zip(source, tmp_path) + assert_result(result).is_success() + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).contains_exactly_in_any_order([ + "addons", + "runtest.cmd", + "runtest.sh", + ]) + + +func test_extract_package_invalid_package() -> void: + clean_temp_dir() + var tmp_path := GdUnitFileAccess.create_temp_dir("test_update") + var source := "res://addons/gdUnit4/test/update/resources/update_invalid.zip" + + # the temp should be inital empty + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).is_empty() + # now extract to temp + var result := GdUnitFileAccess.extract_zip(source, tmp_path) + assert_result(result).is_error()\ + .contains_message("Extracting `%s` failed! Please collect the error log and report this. Error Code: 1" % source) + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).is_empty() + diff --git a/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd.uid b/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd.uid new file mode 100644 index 0000000..1893a6e --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd.uid @@ -0,0 +1 @@ +uid://cij6uh4unqnro diff --git a/addons/gdUnit4/test/core/GdUnitResultTest.gd b/addons/gdUnit4/test/core/GdUnitResultTest.gd new file mode 100644 index 0000000..71e5a3d --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitResultTest.gd @@ -0,0 +1,37 @@ +# GdUnit generated TestSuite +class_name GdUnitResultTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitResult.gd' + + +func test_serde() -> void: + var value := { + "info" : "test", + "meta" : 42 + } + var source := GdUnitResult.success(value) + var serialized_result := GdUnitResult.serialize(source) + var deserialised_result := GdUnitResult.deserialize(serialized_result) + assert_object(deserialised_result)\ + .is_instanceof(GdUnitResult) \ + .is_equal(source) + + +func test_or_else_on_success() -> void: + var result := GdUnitResult.success("some value") + assert_str(result.value()).is_equal("some value") + assert_str(result.or_else("other value")).is_equal("some value") + + +func test_or_else_on_warning() -> void: + var result := GdUnitResult.warn("some warning message") + assert_object(result.value()).is_null() + assert_str(result.or_else("other value")).is_equal("other value") + + +func test_or_else_on_error() -> void: + var result := GdUnitResult.error("some error message") + assert_object(result.value()).is_null() + assert_str(result.or_else("other value")).is_equal("other value") diff --git a/addons/gdUnit4/test/core/GdUnitResultTest.gd.uid b/addons/gdUnit4/test/core/GdUnitResultTest.gd.uid new file mode 100644 index 0000000..6f53a1e --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitResultTest.gd.uid @@ -0,0 +1 @@ +uid://bbu87f4fpu6u diff --git a/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd b/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd new file mode 100644 index 0000000..428fc7d --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd @@ -0,0 +1,76 @@ +# GdUnit generated TestSuite +class_name GdUnitRunnerConfigTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitRunnerConfig.gd' + + +func test_initial_config() -> void: + var config := GdUnitRunnerConfig.new() + assert_array(config.test_cases()).is_empty() + + +func test_clear_on_initial_config() -> void: + var config := GdUnitRunnerConfig.new() + config.clear() + assert_array(config.test_cases()).is_empty() + + +func test_set_server_port() -> void: + var config := GdUnitRunnerConfig.new() + # intial value + assert_int(config.server_port()).is_equal(-1) + + config.set_server_port(1000) + assert_int(config.server_port()).is_equal(1000) + + +func test_load_fail() -> void: + var config := GdUnitRunnerConfig.new() + + assert_result(config.load_config("invalid_path"))\ + .is_warning()\ + .contains_message("Can't find test runner configuration 'invalid_path'! Please select a test to run.") + + +func test_save_load() -> void: + var config := GdUnitRunnerConfig.new() + # add some dummy conf + config.set_server_port(1000) + # create a set of test cases + var test_to_save: Array[GdUnitTestCase] = [ + GdUnitTestCase.from("res://test/example_suite.gd", "res://test/example_suite.gd", 10, "test_a"), + GdUnitTestCase.from("res://test/example_suite.gd", "res://test/example_suite.gd", 14, "test_b"), + GdUnitTestCase.from("res://test/example_suite.gd", "res://test/example_suite.gd", 16, "test_c") + ] + config.add_test_cases(test_to_save) + + var config_file := create_temp_dir("test_save_load") + "/testconf.cfg" + + assert_result(config.save_config(config_file)).is_success() + assert_file(config_file).exists() + + var config2 := GdUnitRunnerConfig.new() + assert_result(config2.load_config(config_file)).is_success() + # verify the config has original enties + assert_str(config2.version()).is_equal(GdUnitRunnerConfig.CONFIG_VERSION) + assert_array(config2.test_cases()).contains_exactly_in_any_order(test_to_save) + + +func test_add_test_cases() -> void: + + var config := GdUnitRunnerConfig.new() + # add some dummy conf + config.set_server_port(1000) + # create a set of test cases + config.add_test_cases([ + GdUnitTestCase.from("res://test/example_suite.gd", "res://test/example_suite.gd", 10, "test_a"), + GdUnitTestCase.from("res://test/example_suite.gd", "res://test/example_suite.gd", 14, "test_b"), + GdUnitTestCase.from("res://test/example_suite.gd", "res://test/example_suite.gd", 16, "test_c") + ]) + + var config_file := create_temp_dir("test_save_load") + "/testconf.cfg" + + assert_result(config.save_config(config_file)).is_success() + assert_file(config_file).exists() diff --git a/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd.uid b/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd.uid new file mode 100644 index 0000000..cd2b4bc --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd.uid @@ -0,0 +1 @@ +uid://btujbk5cqc4qj diff --git a/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd b/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd new file mode 100644 index 0000000..3c50c13 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd @@ -0,0 +1,1062 @@ +@warning_ignore_start("redundant_await", "unsafe_method_access") +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSceneRunner.gd' + + +var _runner :GdUnitSceneRunner +var _scene_spy :Node + + +func before_test() -> void: + _scene_spy = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + _runner = scene_runner(_scene_spy) + # do handle outstanding events + await _runner.await_input_processed() + assert_initial_action_state() + assert_inital_mouse_state() + assert_inital_key_state() + # reset to inital state + reset(_scene_spy) + verify_no_more_interactions(_scene_spy) + + +# asserts to action strings +func assert_initial_action_state() -> void: + for action in InputMap.get_actions(): + assert_bool(Input.is_action_pressed(action, true)).is_false() + + +# asserts to KeyList Enums +func assert_inital_key_state() -> void: + # scacode 4194304-4194415 + for key in range(KEY_SPECIAL, KEY_LAUNCHF): + assert_bool(Input.is_key_pressed(key)).is_false() + assert_bool(Input.is_physical_key_pressed(key)).is_false() + # keycode 32-255 + for key in range(KEY_SPACE, KEY_SECTION): + assert_bool(Input.is_key_pressed(key)).is_false() + assert_bool(Input.is_physical_key_pressed(key)).is_false() + + +#asserts to Mouse ButtonList Enums +func assert_inital_mouse_state() -> void: + for button :int in [ + MOUSE_BUTTON_LEFT, + MOUSE_BUTTON_MIDDLE, + MOUSE_BUTTON_RIGHT, + MOUSE_BUTTON_XBUTTON1, + MOUSE_BUTTON_XBUTTON2, + MOUSE_BUTTON_WHEEL_UP, + MOUSE_BUTTON_WHEEL_DOWN, + MOUSE_BUTTON_WHEEL_LEFT, + MOUSE_BUTTON_WHEEL_RIGHT, + ]: + assert_bool(Input.is_mouse_button_pressed(button))\ + .override_failure_message("Expecting mouse button %s is not pressed state!" % 1)\ + .is_false() + assert_that(Input.get_mouse_button_mask())\ + .override_failure_message("Expecting mouse button mask %s is '0'!" % Input.get_mouse_button_mask())\ + .is_equal(0) + + +func test_reset_to_inital_state_on_release() -> void: + var runner := scene_runner("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + # simulate mouse buttons and key press but we never released it + runner.simulate_action_press("ui_up") + runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + runner.simulate_mouse_button_press(MOUSE_BUTTON_MIDDLE) + runner.simulate_key_press(KEY_0) + runner.simulate_key_press(KEY_X) + await await_idle_frame() + assert_bool(Input.is_action_pressed("ui_up")).is_true() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE)).is_true() + assert_bool(Input.is_key_pressed(KEY_0)).is_true() + assert_bool(Input.is_key_pressed(KEY_X)).is_true() + # unreference the scene runner to enforce reset to initial Input state + runner._notification(NOTIFICATION_PREDELETE) + await await_idle_frame() + assert_bool(Input.is_action_pressed("ui_up")).is_false() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_false() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_false() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE)).is_false() + assert_bool(Input.is_key_pressed(KEY_0)).is_false() + assert_bool(Input.is_key_pressed(KEY_X)).is_false() + + +func test_simulate_action_press() -> void: + # iterate over some example actions + var actions_to_simmulate :Array[String] = ["ui_up", "ui_down", "ui_left", "ui_right"] + for action in actions_to_simmulate: + assert_bool(InputMap.has_action(action)).is_true() + _runner.simulate_action_press(action) + await _runner.await_input_processed() + + assert_bool(Input.is_action_pressed(action))\ + .override_failure_message("Expect the action '%s' is pressed" % action).is_true() + assert_float(Input.get_action_strength(action))\ + .is_equal_approx(1.0, 0.1) + assert_float(_runner.get_property("_last_pressed_strength"))\ + .is_equal_approx(1.0, 0.1) + + # other actions are not pressed + for action :String in ["ui_accept", "ui_select", "ui_cancel"]: + assert_bool(Input.is_action_pressed(action))\ + .override_failure_message("Expect the action '%s' is NOT pressed" % action).is_false() + + +func test_simulate_action_release() -> void: + # iterate over some example actions + var actions_to_simmulate :Array[String] = ["ui_up", "ui_down", "ui_left", "ui_right"] + for action in actions_to_simmulate: + assert_bool(InputMap.has_action(action)).is_true() + _runner.simulate_action_press(action) + await await_idle_frame() + _runner.simulate_action_release(action) + + assert_bool(Input.is_action_just_released(action))\ + .override_failure_message("Expect the action '%s' is released" % action).is_true() + # other actions are not pressed + for action :String in ["ui_accept", "ui_select", "ui_cancel"]: + assert_bool(Input.is_action_pressed(action))\ + .override_failure_message("Expect the action '%s' is NOT pressed" % action).is_false() + + +func test_simulate_key_press() -> void: + # iterate over some example keys + for key :int in [KEY_A, KEY_D, KEY_X, KEY_0]: + _runner.simulate_key_press(key) + await _runner.await_input_processed() + + var event := InputEventKey.new() + event.keycode = key as Key + event.physical_keycode = key as Key + event.unicode = key + event.pressed = true + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_key_pressed(key)).is_true() + # verify all this keys are still handled as pressed + assert_bool(Input.is_key_pressed(KEY_A)).is_true() + assert_bool(Input.is_key_pressed(KEY_D)).is_true() + assert_bool(Input.is_key_pressed(KEY_X)).is_true() + assert_bool(Input.is_key_pressed(KEY_0)).is_true() + # other keys are not pressed + assert_bool(Input.is_key_pressed(KEY_B)).is_false() + assert_bool(Input.is_key_pressed(KEY_G)).is_false() + assert_bool(Input.is_key_pressed(KEY_Z)).is_false() + assert_bool(Input.is_key_pressed(KEY_1)).is_false() + + +# Simulates pressing shift+A +# Verified with "res://addons/gdUnit4/test/core/InputEventTestScene.tscn" +func test_simulate_key_press_SHIFT_and_A() -> void: + # press shift + A + _runner.simulate_key_press(KEY_SHIFT) + _runner.simulate_key_press(KEY_A) + await _runner.await_input_processed() + + # We expect key A is pressed and also the modifier shift key + assert_bool(Input.is_key_pressed(KEY_A)).is_true() + assert_bool(Input.is_key_pressed(KEY_SHIFT)).is_true() + assert_bool(Input.is_key_pressed(KEY_ALT)).is_false() + assert_bool(Input.is_key_pressed(KEY_CTRL)).is_false() + assert_bool(Input.is_key_pressed(KEY_META)).is_false() + + # In addition we need to verify we emit two input events: + # first the shift key is pressing (Shift) + var event := InputEventKey.new() + event.keycode = KEY_SHIFT + event.physical_keycode = KEY_SHIFT + event.unicode = KEY_SHIFT as int + event.pressed = true + event.shift_pressed = false + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + + # second in addition the key A is pressing (Shift+A) + event = InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = true + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + verify_no_more_interactions(_scene_spy) + + +# Simulate pressing key A and shift +# Verified with "res://addons/gdUnit4/test/core/InputEventTestScene.tscn" +func test_simulate_key_press_A_and_SHIFT() -> void: + # press key A + shift + _runner.simulate_key_press(KEY_A) + _runner.simulate_key_press(KEY_SHIFT) + await _runner.await_input_processed() + + # We expect key A is pressed and also the modifier shift key + assert_bool(Input.is_key_pressed(KEY_A)).is_true() + assert_bool(Input.is_key_pressed(KEY_SHIFT)).is_true() + assert_bool(Input.is_key_pressed(KEY_ALT)).is_false() + assert_bool(Input.is_key_pressed(KEY_CTRL)).is_false() + assert_bool(Input.is_key_pressed(KEY_META)).is_false() + + # In addition we need to verify we emit two input events: + # first the shift a is pressing (A) + var event := InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = true + event.shift_pressed = false + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + + # second in addition the key shift is pressing (Shift) + event = InputEventKey.new() + event.keycode = KEY_SHIFT + event.physical_keycode = KEY_SHIFT + event.unicode = KEY_SHIFT as int + event.pressed = true + event.shift_pressed = false + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + verify_no_more_interactions(_scene_spy) + + +# Simulate releasing key shift+A +# Verified with "res://addons/gdUnit4/test/core/InputEventTestScene.tscn" +func test_simulate_key_released_SHIFT_and_A() -> void: + # release shift + A + _runner.simulate_key_release(KEY_SHIFT) + _runner.simulate_key_release(KEY_A) + await _runner.await_input_processed() + + # We expect key A is NOT pressed (it was released) and no modifier keys + assert_bool(Input.is_key_pressed(KEY_A)).is_false() + assert_bool(Input.is_key_pressed(KEY_SHIFT)).is_false() + assert_bool(Input.is_key_pressed(KEY_ALT)).is_false() + assert_bool(Input.is_key_pressed(KEY_CTRL)).is_false() + assert_bool(Input.is_key_pressed(KEY_META)).is_false() + + # In addition we need to verify we emit two input events: + # first the shift+a key is released (Shift+A) + var event := InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = false + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + + # second in addition the shift is released (Shift) + event = InputEventKey.new() + event.keycode = KEY_SHIFT + event.physical_keycode = KEY_SHIFT + event.unicode = KEY_SHIFT as int + event.pressed = false + event.shift_pressed = false + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + verify_no_more_interactions(_scene_spy) + + +# Simulate pressed key shift+A +# Verified with "res://addons/gdUnit4/test/core/InputEventTestScene.tscn" +func test_simulate_key_pressed_SHIFT_and_A() -> void: + # release shift + A + await _runner.simulate_key_pressed(KEY_SHIFT) + await _runner.simulate_key_pressed(KEY_A) + + # We expect key A is NOT pressed (it was press and released) and no modifier keys + assert_bool(Input.is_key_pressed(KEY_A)).is_false() + assert_bool(Input.is_key_pressed(KEY_SHIFT)).is_false() + assert_bool(Input.is_key_pressed(KEY_ALT)).is_false() + assert_bool(Input.is_key_pressed(KEY_CTRL)).is_false() + assert_bool(Input.is_key_pressed(KEY_META)).is_false() + + # In addition, we need to check whether we output two input events: + # first the shift key is pressing (Shift+A) + var event := InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = true + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + + # second in addition the key A is released (Shift+A) + event = InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = false + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + + +# Simulate pressing key A + set shift modifier +# @deprecated +func test_simulate_key_press_A_with_shift_modifier() -> void: + # press key A + modifier shift + _runner.simulate_key_press(KEY_A, true) + await _runner.await_input_processed() + + # We expect key A is pressing only and no modifier keys + assert_bool(Input.is_key_pressed(KEY_A)).is_true() + assert_bool(Input.is_key_pressed(KEY_SHIFT)).is_false() + assert_bool(Input.is_key_pressed(KEY_ALT)).is_false() + assert_bool(Input.is_key_pressed(KEY_CTRL)).is_false() + assert_bool(Input.is_key_pressed(KEY_META)).is_false() + + # In addition we need to verify we emit one input events: + # the key A + modifier shift is pressing (Shift+A) + var event := InputEventKey.new() + event = InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = true + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + verify_no_more_interactions(_scene_spy) + + +# Simulate releasing key A + set shift modifier +# @deprecated +func test_simulate_key_released_A_with_shift_modifier() -> void: + # release key A + modifiere shift + _runner.simulate_key_release(KEY_A, true) + await _runner.await_input_processed() + + # We expect key A is NOT pressing and also no modifier keys + assert_bool(Input.is_key_pressed(KEY_A)).is_false() + assert_bool(Input.is_key_pressed(KEY_SHIFT)).is_false() + assert_bool(Input.is_key_pressed(KEY_ALT)).is_false() + assert_bool(Input.is_key_pressed(KEY_CTRL)).is_false() + assert_bool(Input.is_key_pressed(KEY_META)).is_false() + + # In addition we need to verify we emit one input events: + # The key A and modifier shift is released (Shift+A) + var event := InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = false + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + verify_no_more_interactions(_scene_spy) + + +# Simulate pressed key A + set shift modifier +# @deprecated +func test_simulate_key_pressed_A_with_shift_modifier() -> void: + # release key A + modifier shift + await _runner.simulate_key_pressed(KEY_A, true) + + # We expect key A is NOT pressing (was press and released) and also no modifier keys + assert_bool(Input.is_key_pressed(KEY_SHIFT)).is_false() + assert_bool(Input.is_key_pressed(KEY_ALT)).is_false() + assert_bool(Input.is_key_pressed(KEY_CTRL)).is_false() + assert_bool(Input.is_key_pressed(KEY_META)).is_false() + assert_bool(Input.is_key_pressed(KEY_A)).is_false() + + # In addition, we need to check whether we output two input events: + # first the key a + modifier shift is pressing (Shift+A) + var event := InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = true + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + + # second in addition the key a + modifier shift is released (Shift+A) + event = InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.unicode = KEY_A as int + event.pressed = false + event.shift_pressed = true + event.alt_pressed = false + event.ctrl_pressed = false + event.meta_pressed = false + verify(_scene_spy, 1)._input(event) + verify_no_more_interactions(_scene_spy) + + +func test_simulate_many_keys_press() -> void: + # press and hold keys W and Z + _runner.simulate_key_press(KEY_W) + _runner.simulate_key_press(KEY_Z) + await _runner.await_input_processed() + + assert_bool(Input.is_key_pressed(KEY_W)).is_true() + assert_bool(Input.is_physical_key_pressed(KEY_W)).is_true() + assert_bool(Input.is_key_pressed(KEY_Z)).is_true() + assert_bool(Input.is_physical_key_pressed(KEY_Z)).is_true() + + #now release key w + _runner.simulate_key_release(KEY_W) + await _runner.await_input_processed() + + assert_bool(Input.is_key_pressed(KEY_W)).is_false() + assert_bool(Input.is_physical_key_pressed(KEY_W)).is_false() + assert_bool(Input.is_key_pressed(KEY_Z)).is_true() + assert_bool(Input.is_physical_key_pressed(KEY_Z)).is_true() + + +@warning_ignore("unsafe_property_access") +func test_simulate_keypressed_as_action() -> void: + # add custom action `player_jump` for key 'Space' is pressed + var event := InputEventKey.new() + event.keycode = KEY_SPACE + InputMap.add_action("player_jump") + InputMap.action_add_event("player_jump", event) + var runner := scene_runner("res://addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn") + + # precondition checks + var action_event := InputMap.action_get_events("player_jump") + assert_array(action_event).contains_exactly([event]) + assert_bool(Input.is_action_just_released("player_jump", true)).is_false() + assert_bool(Input.is_action_just_released("ui_accept", true)).is_false() + assert_bool(Input.is_action_just_released("ui_select", true)).is_false() + @warning_ignore("unsafe_property_access") + assert_bool(runner.scene()._player_jump_action_released).is_false() + + await runner.simulate_key_pressed(KEY_SPACE) + # it is important do not wait for next frame here, otherwise the input action cache is cleared and can't be use to verify + assert_bool(Input.is_action_just_released("player_jump", true)).is_true() + assert_bool(Input.is_action_just_released("ui_accept", true)).is_true() + assert_bool(Input.is_action_just_released("ui_select", true)).is_true() + @warning_ignore("unsafe_property_access") + assert_bool(runner.scene()._player_jump_action_released).is_true() + + # test a key event is not trigger the custom action event + # simulate press only space+ctrl + runner._reset_input_to_default() + # @deprecated + runner.simulate_key_pressed(KEY_SPACE, false, true) + # it is important do not wait for next frame here, otherwise the input action cache is cleared and can't be use to verify + assert_bool(Input.is_action_just_released("player_jump", true)).is_false() + assert_bool(Input.is_action_just_released("ui_accept", true)).is_false() + assert_bool(Input.is_action_just_released("ui_select", true)).is_false() + @warning_ignore("unsafe_property_access") + assert_bool(runner.scene()._player_jump_action_released).is_false() + + # cleanup custom action + InputMap.action_erase_events("player_jump") + InputMap.erase_action("player_jump") + + +func test_simulate_set_mouse_pos() -> void: + # save current global mouse pos + var gmp := _runner.get_global_mouse_position() + # set mouse to pos 100, 100 + _runner.set_mouse_position(Vector2(100, 100)) + await _runner.await_input_processed() + var event := InputEventMouseMotion.new() + event.position = Vector2(100, 100) + GodotVersionFixures.set_event_global_position(event, gmp) + verify(_scene_spy, 1)._input(event) + + # set mouse to pos 800, 400 + gmp = _runner.get_global_mouse_position() + _runner.set_mouse_position(Vector2(800, 400)) + await _runner.await_input_processed() + event = InputEventMouseMotion.new() + event.position = Vector2(800, 400) + GodotVersionFixures.set_event_global_position(event, gmp) + verify(_scene_spy, 1)._input(event) + + # and again back to 100,100 + reset(_scene_spy) + gmp = _runner.get_global_mouse_position() + _runner.set_mouse_position(Vector2(100, 100)) + await _runner.await_input_processed() + event = InputEventMouseMotion.new() + event.position = Vector2(100, 100) + GodotVersionFixures.set_event_global_position(event, gmp) + verify(_scene_spy, 1)._input(event) + + +func test_simulate_set_mouse_pos_with_modifiers() -> void: + var is_alt := false + var is_control := false + var is_shift := false + + for modifier :int in [KEY_SHIFT, KEY_CTRL, KEY_ALT]: + is_alt = is_alt or KEY_ALT == modifier + is_control = is_control or KEY_CTRL == modifier + is_shift = is_shift or KEY_SHIFT == modifier + + for mouse_button :int in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + # simulate press shift, set mouse pos and final press mouse button + var gmp := _runner.get_global_mouse_position() + _runner.simulate_key_press(modifier) + _runner.set_mouse_position(Vector2.ZERO) + _runner.simulate_mouse_button_press(mouse_button) + await _runner.await_input_processed() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.alt_pressed = is_alt + event.ctrl_pressed = is_control + event.shift_pressed = is_shift + event.pressed = true + event.button_index = mouse_button as MouseButton + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(mouse_button)).is_true() + # finally release it + _runner.simulate_mouse_button_release(mouse_button) + await _runner.await_input_processed() + + +func test_simulate_mouse_move() -> void: + _runner.set_mouse_position(Vector2(10, 10)) + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_move(Vector2(400, 100)) + await _runner.await_input_processed() + + var event := InputEventMouseMotion.new() + event.position = Vector2(400, 100) + GodotVersionFixures.set_event_global_position(event, gmp) + event.relative = Vector2(400, 100) - Vector2(10, 10) + verify(_scene_spy, 1)._input(event) + + # move mouse to next pos + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_move(Vector2(55, 42)) + await await_idle_frame() + + event = InputEventMouseMotion.new() + event.position = Vector2(55, 42) + GodotVersionFixures.set_event_global_position(event, gmp) + event.relative = Vector2(55, 42) - Vector2(400, 100) + verify(_scene_spy, 1)._input(event) + + +func test_simulate_mouse_move_relative() -> void: + #OS.window_minimized = false + _runner.set_mouse_position(Vector2(10, 10)) + await _runner.await_input_processed() + assert_that(_runner.get_mouse_position()).is_equal(Vector2(10, 10)) + + # move the mouse in time of 1 second + # the final position is current + relative = Vector2(10, 10) + (Vector2(900, 400) + await _runner.simulate_mouse_move_relative(Vector2(900, 400), 1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(910, 410), Vector2.ONE) + + # move the mouse back in time of 0.1 second + # Use the negative value of the previously moved action to move it back to the starting position + await _runner.simulate_mouse_move_relative(Vector2(-900, -400), 0.1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(10, 10), Vector2.ONE) + + +func test_simulate_mouse_move_absolute() -> void: + #OS.window_minimized = false + _runner.set_mouse_position(Vector2(10, 10)) + await _runner.await_input_processed() + assert_that(_runner.get_mouse_position()).is_equal(Vector2(10, 10)) + + # move the mouse in time of 1 second + await _runner.simulate_mouse_move_absolute(Vector2(900, 400), 1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(900, 400), Vector2.ONE) + + # move the mouse back in time of 0.1 second + await _runner.simulate_mouse_move_absolute(Vector2(10, 10), 0.1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(10, 10), Vector2.ONE) + + +func test_simulate_mouse_button_press_left() -> void: + # simulate mouse button press and hold + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + await _runner.await_input_processed() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(MOUSE_BUTTON_LEFT) + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + + +func test_simulate_mouse_button_press_left_doubleclick() -> void: + # simulate mouse button press double_click + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT, true) + await _runner.await_input_processed() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.double_click = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(MOUSE_BUTTON_LEFT) + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + + +func test_simulate_mouse_button_press_right() -> void: + # simulate mouse button press and hold + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + await _runner.await_input_processed() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(MOUSE_BUTTON_RIGHT) + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + + +func test_simulate_mouse_button_press_left_and_right() -> void: + # simulate mouse button press left+right + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + _runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + await _runner.await_input_processed() + + # results in two events, first is left mouse button + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = MOUSE_BUTTON_MASK_LEFT + verify(_scene_spy, 1)._input(event) + + # second is left+right and combined mask + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + assert_that(Input.get_mouse_button_mask()).is_equal(MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT) + + +func test_simulate_mouse_button_press_left_and_right_and_release() -> void: + # simulate mouse button press left+right + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + _runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + await _runner.await_input_processed() + + # will results into two events + # first for left mouse button + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = MOUSE_BUTTON_MASK_LEFT + verify(_scene_spy, 1)._input(event) + + # second is left+right and combined mask + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + assert_that(Input.get_mouse_button_mask()).is_equal(MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT) + + # now release the right button + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_RIGHT) + await _runner.await_input_processed() + # will result in right button press false but stay with mask for left pressed + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = MOUSE_BUTTON_MASK_LEFT + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_false() + assert_that(Input.get_mouse_button_mask()).is_equal(MOUSE_BUTTON_MASK_LEFT) + + # finally relase left button + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await _runner.await_input_processed() + # will result in right button press false but stay with mask for left pressed + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_false() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_false() + assert_that(Input.get_mouse_button_mask()).is_equal(0) + + +func test_simulate_mouse_button_pressed() -> void: + for mouse_button :int in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + # simulate mouse button press and release + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(mouse_button) + await _runner.await_input_processed() + + # it genrates two events, first for press and second as released + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = mouse_button as MouseButton + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = mouse_button as MouseButton + event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(mouse_button)).is_false() + verify(_scene_spy, 2)._input(any_class(InputEventMouseButton)) + reset(_scene_spy) + + +func test_simulate_mouse_button_pressed_doubleclick() -> void: + for mouse_button :int in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + # simulate mouse button press and release by double_click + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(mouse_button, true) + await _runner.await_input_processed() + + # it genrates two events, first for press and second as released + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.double_click = true + event.button_index = mouse_button as MouseButton + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.double_click = false + event.button_index = mouse_button as MouseButton + event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(mouse_button)).is_false() + verify(_scene_spy, 2)._input(any_class(InputEventMouseButton)) + reset(_scene_spy) + + +func test_simulate_mouse_button_press_and_release() -> void: + for mouse_button :int in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + var gmp := _runner.get_global_mouse_position() + # simulate mouse button press and release + _runner.simulate_mouse_button_press(mouse_button) + await _runner.await_input_processed() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = mouse_button as MouseButton + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(mouse_button)).is_true() + + # now simulate mouse button release + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_button_release(mouse_button) + await _runner.await_input_processed() + + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = mouse_button as MouseButton + #event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_bool(Input.is_mouse_button_pressed(mouse_button)).is_false() + + +##################################################################################################################### +# Tests of simulate touch screen inputs # +##################################################################################################################### +func test_simulate_screen_touch_press() -> void: + # simulate pressing the touching screen + _runner.simulate_screen_touch_press(0, Vector2(683, 339)) + await _runner.await_input_processed() + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(683, 339)) + var event := InputEventScreenTouch.new() + event.index = 0 + event.position = Vector2(683, 339) + event.pressed = true + event.double_tap = false + verify(_scene_spy, 1)._input(event) + verify(_scene_spy, 1)._on_touch_1_pressed() + verify(_scene_spy, 0)._on_touch_1_released() + + +func test_simulate_screen_touch_press_double_click() -> void: + # simulate pressing the touch screen by a double click + _runner.simulate_screen_touch_press(0, Vector2(683, 339), true) + await _runner.await_input_processed() + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(683, 339)) + var event := InputEventScreenTouch.new() + event.index = 0 + event.position = Vector2(683, 339) + event.pressed = true + event.double_tap = true + verify(_scene_spy, 1)._input(event) + verify(_scene_spy, 1)._on_touch_1_pressed() + verify(_scene_spy, 0)._on_touch_1_released() + + +func test_simulate_screen_touch_pressed() -> void: + # simulate has touched the screen + _runner.simulate_screen_touch_pressed(0, Vector2(683, 339)) + await _runner.await_input_processed() + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(683, 339)) + var event := InputEventScreenTouch.new() + event.index = 0 + event.position = Vector2(683, 339) + event.pressed = true + event.double_tap = false + verify(_scene_spy, 1)._input(event) + event.pressed = false + verify(_scene_spy, 1)._input(event) + verify(_scene_spy, 1)._on_touch_1_pressed() + verify(_scene_spy, 1)._on_touch_1_released() + + +func test_simulate_screen_touch_pressed_double_click() -> void: + # simulate have touched the touch screen by a double click + _runner.simulate_screen_touch_pressed(0, Vector2(683, 339), true) + await _runner.await_input_processed() + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(683, 339)) + verify(_scene_spy, 1)._on_touch_1_pressed() + verify(_scene_spy, 1)._on_touch_1_released() + + +func test_simulate_screen_touch_release() -> void: + # setup touch is actual pressing + _runner.simulate_screen_touch_press(0, Vector2(683, 339)) + # simulate no longer touches the screen + _runner.simulate_screen_touch_release(0) + await _runner.await_input_processed() + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(683, 339)) + var event := InputEventScreenTouch.new() + event.index = 0 + event.position = Vector2(683, 339) + event.pressed = false + event.double_tap = false + verify(_scene_spy, 1)._input(event) + verify(_scene_spy, 1)._on_touch_1_released() + + +func test_simulate_screen_touch_release_double_click() -> void: + # setup touch is actual pressing + _runner.simulate_screen_touch_press(0, Vector2(683, 339), true) + # simulate that no longer touches the screen as a double click + _runner.simulate_screen_touch_release(0, true) + await _runner.await_input_processed() + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(683, 339)) + var event := InputEventScreenTouch.new() + event.index = 0 + event.position = Vector2(683, 339) + event.pressed = false + event.double_tap = true + verify(_scene_spy, 1)._input(event) + verify(_scene_spy, 1)._on_touch_1_released() + + +func test_simulate_screen_touch_get_drag_position() -> void: + # press the touch screen by two fingers + _runner.simulate_screen_touch_press(0, Vector2(300, 100)) + _runner.simulate_screen_touch_press(1, Vector2(300, 200)) + + # verify the drag position is saved for each index + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(300, 100)) + assert_that(_runner.get_screen_touch_drag_position(1)).is_equal(Vector2(300, 200)) + + +func test_simulate_screen_touch_drag() -> void: + _runner.simulate_screen_touch_drag(0, Vector2(300, 100)) + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(300, 100)) + var event := InputEventScreenDrag.new() + event.index = 0 + event.position = Vector2(300, 100) + event.relative = Vector2.ZERO + event.pressure = 1.0 + verify(_scene_spy, 1)._input(event) + + # drag to next position + _runner.simulate_screen_touch_drag(0, Vector2(400, 100)) + + # verify the InputEventScreenTouch is emitted + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(400, 100)) + event.index = 0 + event.position = Vector2(400, 100) + event.relative = Vector2(300, 100) - Vector2(400, 100) + event.velocity = event.relative / get_tree().root.get_process_delta_time() + event.pressure = 1.0 + verify(_scene_spy, 1)._input(event) + + +# Simulates a gesture in which two fingers are used as input +func test_simulate_screen_touch_gesture_press() -> void: + # simulate gesture with two fingers is touching the screen + # finger one has index=0 + _runner.simulate_screen_touch_press(0, Vector2(300, 100)) + # finger one has index=1 + _runner.simulate_screen_touch_press(1, Vector2(300, 200)) + + # verify the InputEventScreenTouch is emitted for finger one + var touch_fg1 := InputEventScreenTouch.new() + touch_fg1.index = 0 + touch_fg1.position = Vector2(300, 100) + touch_fg1.pressed = true + touch_fg1.double_tap = false + verify(_scene_spy, 1)._input(touch_fg1) + + # and verify the InputEventScreenTouch is emitted for finger one + var touch_fg2 := InputEventScreenTouch.new() + touch_fg2.index = 1 + touch_fg2.position = Vector2(300, 200) + touch_fg2.pressed = true + touch_fg2.double_tap = false + verify(_scene_spy, 1)._input(touch_fg2) + + +# Simulates a gesture in which two fingers are used as input +func test_simulate_screen_touch_gesture_release() -> void: + # setup two fingers are pressing the touch screen + # finger one has index=0 + # finger one has index=1 + _runner.simulate_screen_touch_press(0, Vector2(300, 100)) + _runner.simulate_screen_touch_press(1, Vector2(300, 200)) + + # simulate gesture with two fingers is untouch the screen + _runner.simulate_screen_touch_release(0) + _runner.simulate_screen_touch_release(1) + + # verify the InputEventScreenTouch is emitted for finger one + var touch_fg1 := InputEventScreenTouch.new() + touch_fg1.index = 0 + touch_fg1.position = Vector2(300, 100) + touch_fg1.pressed = false + touch_fg1.double_tap = false + verify(_scene_spy, 1)._input(touch_fg1) + + # and verify the InputEventScreenTouch is emitted for finger one + var touch_fg2 := InputEventScreenTouch.new() + touch_fg2.index = 1 + touch_fg2.position = Vector2(300, 200) + touch_fg2.pressed = false + touch_fg2.double_tap = false + verify(_scene_spy, 1)._input(touch_fg2) + + +# Simulates a gesture in which two fingers are used as input +func test_simulate_screen_touch_gesture_zoom_out() -> void: + # setup two fingers are pressing the touch screen + # finger one has index=0 + # finger one has index=1 + var finger_one := Vector2(300, 200) + var finger_two := Vector2(300, 250) + _runner.simulate_screen_touch_press(0, finger_one) + _runner.simulate_screen_touch_press(1, finger_two) + + # now simulate gestures by moving the points of contact away from each other + for y_pos in range(0, 105, 5): + _runner.simulate_screen_touch_drag(0, finger_one - Vector2(0, y_pos)) + _runner.simulate_screen_touch_drag(1, finger_two + Vector2(0, y_pos)) + + # verify final position are correct + assert_that(_runner.get_screen_touch_drag_position(0)).is_equal(Vector2(300, 100)) + assert_that(_runner.get_screen_touch_drag_position(1)).is_equal(Vector2(300, 350)) + + +func test_text_input_processing() -> void: + _runner.move_window_to_foreground() + var lineEdit := _runner.find_child("TextInput") as LineEdit + + # Focus the text input and clear any existing content + lineEdit.grab_focus() + assert_bool(lineEdit.has_focus()).is_true() + lineEdit.text = "" + + # Type individual characters + _runner.simulate_key_pressed(KEY_H) + _runner.simulate_key_pressed(KEY_I) + await _runner.await_input_processed() + await _runner.simulate_frames(100) + + # Verify text accumulation + assert_that(lineEdit.text).is_equal("HI"); diff --git a/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd.uid b/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd.uid new file mode 100644 index 0000000..085c369 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd.uid @@ -0,0 +1 @@ +uid://btf7v4w5sjeyk diff --git a/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd b/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd new file mode 100644 index 0000000..e3aae49 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd @@ -0,0 +1,489 @@ +# GdUnit generated TestSuite +@warning_ignore_start("redundant_await", "unsafe_method_access") +class_name GdUnitSceneRunnerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd' + + +# loads the test runner and register for auto freeing after test +func load_test_scene() -> Node: + @warning_ignore("unsafe_method_access") + return auto_free(load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn").instantiate()) + + +func before() -> void: + # use a dedicated FPS because we calculate frames by time + Engine.set_max_fps(60) + + +func after() -> void: + Engine.set_max_fps(0) + + +func test_get_property() -> void: + var runner := scene_runner(load_test_scene()) + + assert_that(runner.get_property("_box1")).is_instanceof(ColorRect) + assert_that(runner.get_property("_invalid")).is_equal("The property '_invalid' not exist checked loaded scene.") + assert_that(runner.get_property("_nullable")).is_null() + + +func test_set_property() -> void: + var runner := scene_runner(load_test_scene()) + + assert_that(runner.set_property("_invalid", 42)).is_equal(false) + + assert_that(runner.set_property("_nullable", RefCounted.new())).is_equal(true) + assert_that(runner.get_property("_nullable")).is_instanceof(RefCounted) + +func test_invoke_method() -> void: + var runner := scene_runner(load_test_scene()) + + assert_that(runner.invoke("add", 10, 12)).is_equal(22) + assert_that(runner.invoke("sub", 10, 12)).is_equal("The method 'sub' not exist checked loaded scene.") + + +@warning_ignore("unused_parameter") +func test_simulate_frames(timeout := 5000) -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # initial is white + assert_object(box1.color).is_equal(Color.WHITE) + + # start color cycle by invoke the function 'start_color_cycle' + runner.invoke("start_color_cycle") + + # we wait for 10 frames + await runner.simulate_frames(10) + # after 10 frame is still white + assert_object(box1.color).is_equal(Color.WHITE) + + # we wait 30 more frames + await runner.simulate_frames(30) + # after 40 frames the box one should be changed to red + assert_object(box1.color).is_equal(Color.RED) + + +@warning_ignore("unused_parameter") +func test_simulate_frames_withdelay(timeout := 4000) -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # initial is white + assert_object(box1.color).is_equal(Color.WHITE) + + # start color cycle by invoke the function 'start_color_cycle' + runner.invoke("start_color_cycle") + + # we wait for 10 frames each with a 50ms delay + await runner.simulate_frames(10, 50) + # after 10 frame and in sum 500ms is should be changed to red + assert_object(box1.color).is_equal(Color.RED) + + +@warning_ignore("unused_parameter") +func test_run_scene_colorcycle(timeout := 2000) -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # verify inital color + assert_object(box1.color).is_equal(Color.WHITE) + + # start color cycle by invoke the function 'start_color_cycle' + runner.invoke("start_color_cycle") + + # await for each color cycle is emited + await runner.await_signal("panel_color_change", [box1, Color.RED]) + assert_object(box1.color).is_equal(Color.RED) + await runner.await_signal("panel_color_change", [box1, Color.BLUE]) + assert_object(box1.color).is_equal(Color.BLUE) + await runner.await_signal("panel_color_change", [box1, Color.GREEN]) + assert_object(box1.color).is_equal(Color.GREEN) + + +func test_simulate_scene_inteaction_by_press_enter(timeout := 2000) -> void: + var runner := scene_runner(load_test_scene()) + + # inital no spell is fired + assert_object(runner.find_child("Spell")).is_null() + + # fire spell be pressing enter key + runner.simulate_key_pressed(KEY_ENTER) + # wait until next frame + await await_idle_frame() + + # verify a spell is created + assert_object(runner.find_child("Spell")).is_not_null() + + # wait until spell is explode after around 1s + var spell := runner.find_child("Spell") + if spell == null: + return + await await_signal_on(spell, "spell_explode", [spell], timeout) + + # verify spell is removed when is explode + assert_object(runner.find_child("Spell")).is_null() + + +# mock on a runner and spy on created spell +func test_simulate_scene_inteaction_in_combination_with_spy() -> void: + var spy_: Object = spy(load_test_scene()) + # create a runner runner + var runner := scene_runner(spy_) + + # simulate a key event to fire a spell + await runner.simulate_key_pressed(KEY_ENTER) + verify(spy_).create_spell() + + var spell := runner.find_child("Spell") + assert_that(spell).is_not_null() + assert_that(spell.is_connected("spell_explode", Callable(spy_, "_destroy_spell"))).is_true() + + +func test_simulate_scene_interact_with_buttons() -> void: + var spyed_scene :Variant = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var runner := scene_runner(spyed_scene) + # test button 1 interaction + await await_millis(1000) + runner.set_mouse_position(Vector2(60, 20)) + runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await await_idle_frame() + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box1, Color.RED) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box1, Color.GRAY) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box2, any_color()) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box3, any_color()) + + # test button 2 interaction + reset(spyed_scene) + await await_millis(1000) + runner.set_mouse_position(Vector2(160, 20)) + runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await await_idle_frame() + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box1, any_color()) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box2, Color.RED) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box2, Color.GRAY) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box3, any_color()) + + # test button 3 interaction (is changed after 1s to gray) + reset(spyed_scene) + await await_millis(1000) + runner.set_mouse_position(Vector2(260, 20)) + runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await await_idle_frame() + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box1, any_color()) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box2, any_color()) + # is changed to red + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box3, Color.RED) + # no gray + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box3, Color.GRAY) + # after one second is changed to gray + await await_millis(1200) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box3, Color.GRAY) + + +func test_await_func_without_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + await runner.await_func("color_cycle").is_equal("black") + + +func test_await_func_with_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + + # set max time factor to minimize waiting time checked `runner.wait_func` + runner.set_time_factor(10) + await runner.await_func("color_cycle").wait_until(200).is_equal("black") + + +func test_await_signal_without_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + + runner.invoke("start_color_cycle") + await runner.await_signal("panel_color_change", [box1, Color.RED]) + await runner.await_signal("panel_color_change", [box1, Color.BLUE]) + await runner.await_signal("panel_color_change", [box1, Color.GREEN]) + ( + # should be interrupted is will never change to Color.KHAKI + await assert_failure_await(func x() -> void: await runner.await_signal( "panel_color_change", [box1, Color.KHAKI], 300)) + ).has_message("await_signal_on(panel_color_change, [%s, %s]) timed out after 300ms" % [str(box1), str(Color.KHAKI)])\ + .has_line(207) + + +func test_await_signal_with_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # set max time factor to minimize waiting time checked `runner.wait_func` + runner.set_time_factor(10) + runner.invoke("start_color_cycle") + + await runner.await_signal("panel_color_change", [box1, Color.RED], 100) + await runner.await_signal("panel_color_change", [box1, Color.BLUE], 100) + await runner.await_signal("panel_color_change", [box1, Color.GREEN], 100) + ( + # should be interrupted is will never change to Color.KHAKI + await assert_failure_await(func x() -> void: await runner.await_signal("panel_color_change", [box1, Color.KHAKI], 30)) + ).has_message("await_signal_on(panel_color_change, [%s, %s]) timed out after 30ms" % [str(box1), str(Color.KHAKI)])\ + .has_line(224) + + +func test_simulate_until_signal() -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + + # set max time factor to minimize waiting time checked `runner.wait_func` + runner.invoke("start_color_cycle") + + await runner.simulate_until_signal("panel_color_change", box1, Color.RED) + await runner.simulate_until_signal("panel_color_change", box1, Color.BLUE) + await runner.simulate_until_signal("panel_color_change", box1, Color.GREEN) + + +@warning_ignore("unused_parameter") +func test_simulate_until_object_signal(timeout := 2000) -> void: + var runner := scene_runner(load_test_scene()) + + # inital no spell is fired + assert_object(runner.find_child("Spell")).is_null() + + # fire spell be pressing enter key + runner.simulate_key_pressed(KEY_ENTER) + # wait until next frame + await await_idle_frame() + var spell := runner.find_child("Spell") + + # simmulate scene until the spell is explode + await runner.simulate_until_object_signal(spell, "spell_explode", spell) + + # verify spell is removed when is explode + assert_object(runner.find_child("Spell")).is_null() + + +func test_runner_by_null_instance() -> void: + var runner :GdUnitSceneRunnerImpl = scene_runner(null) + assert_object(runner._current_scene).is_null() + + +func test_runner_by_invalid_resource_path() -> void: + # not existing scene + @warning_ignore("unsafe_property_access") + assert_object(scene_runner("res://test_scene.tscn")._current_scene).is_null() + # not a path to a scene + @warning_ignore("unsafe_property_access") + assert_object(scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.gd")._current_scene).is_null() + + +func test_runner_by_invalid_uid_path() -> void: + # not existing scene + @warning_ignore("unsafe_property_access") + assert_object(scene_runner("uid://invalid_uid")._current_scene).is_null() + + +func test_runner_by_uid_path() -> void: + # uid is for res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn + var runner := scene_runner("uid://cn8ucy2rheu0f") + assert_object(runner.scene()).is_instanceof(Node2D) + + # verify the scene is freed when the runner is freed + var scene := runner.scene() + assert_bool(is_instance_valid(scene)).is_true() + runner._notification(NOTIFICATION_PREDELETE) + # give engine time to free the resources + await await_idle_frame() + # verify runner and scene is freed + assert_bool(is_instance_valid(scene)).is_false() + + +func test_runner_by_binary_resource_path() -> void: + # Hides In external resource #1, invalid UID: 'uid://tyt68dhy4ubc' + Engine.print_error_messages = false + var runner := scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.scn") + Engine.print_error_messages = true + assert_object(runner.scene()).is_instanceof(Node2D) + + # verify the scene is freed when the runner is freed + var scene := runner.scene() + assert_bool(is_instance_valid(scene)).is_true() + runner._notification(NOTIFICATION_PREDELETE) + # give engine time to free the resources + await await_idle_frame() + # verify runner and scene is freed + assert_bool(is_instance_valid(scene)).is_false() + + +func test_runner_by_resource_path() -> void: + var runner := scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn") + assert_object(runner.scene()).is_instanceof(Node2D) + + # verify the scene is freed when the runner is freed + var scene := runner.scene() + assert_bool(is_instance_valid(scene)).is_true() + runner._notification(NOTIFICATION_PREDELETE) + # give engine time to free the resources + await await_idle_frame() + # verify runner and scene is freed + assert_bool(is_instance_valid(scene)).is_false() + + +func test_runner_by_invalid_scene_instance() -> void: + var scene := RefCounted.new() + var runner: GdUnitSceneRunnerImpl = scene_runner(scene) + assert_object(runner._current_scene).is_null() + + +func test_runner_by_scene_instance() -> void: + var scene :Node = load("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn").instantiate() + var runner := scene_runner(scene) + assert_object(runner.scene()).is_instanceof(Node2D) + + # verify the scene is freed when the runner is freed + runner._notification(NOTIFICATION_PREDELETE) + # give engine time to free the resources + await await_idle_frame() + # scene runner using external scene do not free the scene at exit + assert_bool(is_instance_valid(scene)).is_true() + # needs to be manually freed + scene.free() + + +func test_mouse_drag_and_drop() -> void: + var spy_scene :Variant = spy("res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn") + var runner := scene_runner(spy_scene) + + var slot_left :TextureRect = $"/root/DragAndDropScene/left/TextureRect" + var slot_right :TextureRect = $"/root/DragAndDropScene/right/TextureRect" + + var save_mouse_pos := get_tree().root.get_mouse_position() + # set inital mouse pos over the left slot + var mouse_pos := slot_left.global_position + Vector2(50, 50) + runner.set_mouse_position(mouse_pos) + #await await_millis(1000) + + await await_idle_frame() + var event := InputEventMouseMotion.new() + event.position = mouse_pos + event.global_position = save_mouse_pos + verify(spy_scene, 1)._gui_input(event) + + runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + await await_idle_frame() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + + # start drag&drop to left pannel + for i in 20: + runner.simulate_mouse_move(mouse_pos + Vector2(i*.4*i, 0)) + await await_millis(40) + + runner.simulate_mouse_button_release(MOUSE_BUTTON_LEFT) + await await_idle_frame() + assert_that(slot_right.texture).is_equal(slot_left.texture) + + +func test_touch_drag_and_drop_relative() -> void: + var spy_scene :Variant = spy("res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn") + var runner := scene_runner(spy_scene) + + var slot_left :TextureRect = $"/root/DragAndDropScene/left/TextureRect" + var slot_right :TextureRect = $"/root/DragAndDropScene/right/TextureRect" + var drag_start := slot_left.global_position + Vector2(50, 50) + + # set inital mouse pos over the left touch button + runner.simulate_screen_touch_press(0, drag_start) + await await_idle_frame() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_that(get_tree().root.get_mouse_position()).is_equal(drag_start) + + # enable only for a small wait to for manual testing + #await await_millis(1000) + + # start drag&drop to right touch button + var drag_end := drag_start + Vector2(140, 0) + await runner.simulate_screen_touch_drag_relative(0, Vector2(140, 0)) + # verify + assert_that(slot_right.texture).is_equal(slot_left.texture) + assert_that(get_tree().root.get_mouse_position()).is_equal(drag_end) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_false() + + +func test_touch_drag_and_drop_absolute() -> void: + var spy_scene :Variant = spy("res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn") + var runner := scene_runner(spy_scene) + + var slot_left :TextureRect = $"/root/DragAndDropScene/left/TextureRect" + var slot_right :TextureRect = $"/root/DragAndDropScene/right/TextureRect" + var drag_start := slot_left.global_position + Vector2(50, 50) + + # set inital mouse pos over the left touch button + runner.simulate_screen_touch_press(0, drag_start) + await await_idle_frame() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_that(get_tree().root.get_mouse_position()).is_equal(drag_start) + + # enable only for a small wait to for manual testing + #await await_millis(1000) + + # start drag&drop to right touch button + var drag_end := Vector2(drag_start.x+140, drag_start.y) + await runner.simulate_screen_touch_drag_absolute(0, drag_end) + # verify + assert_that(slot_right.texture).is_equal(slot_left.texture) + assert_that(get_tree().root.get_mouse_position()).is_equal(drag_end) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_false() + + +func test_touch_drag_and_drop() -> void: + var spy_scene :Variant = spy("res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn") + var runner := scene_runner(spy_scene) + var slot_left :TextureRect = $"/root/DragAndDropScene/left/TextureRect" + var slot_right :TextureRect = $"/root/DragAndDropScene/right/TextureRect" + var drag_start := slot_left.global_position + Vector2(50, 50) + + # start drag&drop to right touch button + var drag_end := Vector2(drag_start.x+140, drag_start.y) + await runner.simulate_screen_touch_drag_drop(0, drag_start, drag_end) + # verify + assert_that(slot_right.texture).is_equal(slot_left.texture) + assert_that(get_tree().root.get_mouse_position()).is_equal(drag_end) + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_false() + + +func test_runner_GD_356() -> void: + # to avoid reporting the expected push_error as test failure we disable it + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + var runner := scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn") + var player: Object = runner.invoke("find_child", "Player", true, false) + assert_that(player).is_not_null() + await assert_func(player, "is_on_floor").wait_until(500).is_true() + assert_that(runner.scene()).is_not_null() + # run simulate_mouse_move_relative without await to reproduce https://github.com/MikeSchulze/gdUnit4/issues/356 + # this results into releasing the scene while `simulate_mouse_move_relative` is processing the mouse move + runner.simulate_mouse_move_relative(Vector2(100, 100), 1.0) + assert_that(runner.scene()).is_not_null() + + +func test_move_window_to_foreground() -> void: + var runner := scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn") + # we set inital to background + runner.move_window_to_background() + await runner.simulate_frames(1) + if not Engine.is_embedded_in_editor(): + # We need a workaround because of https://github.com/godotengine/godot/issues/111119 + var expecetd_mode := DisplayServer.WINDOW_MODE_WINDOWED if OS.get_name() == "Linux" else DisplayServer.WINDOW_MODE_MINIMIZED + assert_that(DisplayServer.window_get_mode()).is_equal(expecetd_mode) + else: + assert_that(DisplayServer.window_get_mode()).is_equal(DisplayServer.WINDOW_MODE_WINDOWED) + # force window to foreground + runner.move_window_to_foreground() + await runner.simulate_frames(1) + assert_that(DisplayServer.window_get_mode()).is_equal(DisplayServer.WINDOW_MODE_WINDOWED) + + +# https://github.com/MikeSchulze/gdUnit4/issues/1020 +func test_load_scene_with_audio_player() -> void: + var runner := scene_runner("res://addons/gdUnit4/test/core/resources/scenes/scene_audio.tscn") + + await runner.simulate_frames(1) + + +# we override the scene runner function for test purposes to hide push_error notifications +func scene_runner(scene :Variant, verbose := false) -> GdUnitSceneRunner: + return auto_free(GdUnitSceneRunnerImpl.new(scene, verbose, true)) diff --git a/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd.uid b/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd.uid new file mode 100644 index 0000000..59d010f --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd.uid @@ -0,0 +1 @@ +uid://krk86dn1g2wr diff --git a/addons/gdUnit4/test/core/GdUnitSettingsTest.gd b/addons/gdUnit4/test/core/GdUnitSettingsTest.gd new file mode 100644 index 0000000..d617976 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSettingsTest.gd @@ -0,0 +1,289 @@ +# GdUnit generated TestSuite +class_name GdUnitSettingsTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSettings.gd' + +const IGNORE := GdUnitSettings.GdScriptWarningMode.IGNORE +const WARN := GdUnitSettings.GdScriptWarningMode.WARN +const ERROR := GdUnitSettings.GdScriptWarningMode.ERROR +const EXCLUDE := GdUnitSettings.GdScriptWarningDirectoryMode.EXCLUDE +const INCLUDE := GdUnitSettings.GdScriptWarningDirectoryMode.INCLUDE + + +static func get_godot_property_info(property_name: String) -> Dictionary: + for property: Dictionary in ProjectSettings.get_property_list(): + if property["name"] == property_name: + return property + return {} + + +#region list_settings +func test_list_settings() -> void: + var report_errors := "unit_test/settings/report_errors" + var report_warnings := "unit_test/settings/report_warnings" + var max_retries := "unit_test/settings/max_retries" + var enable_logging := "unit_test/network/enable_logging" + var verbose_output := "unit_test/network/verbose_output" + var log_path := "unit_test/network/log_path" + var timeout_seconds := "unit_test/network/timeout_seconds" + GdUnitSettings.create_property_if_need(report_errors, true, "Report errors as failures") + GdUnitSettings.create_property_if_need(report_warnings, false, "Report warnings as failures") + GdUnitSettings.create_property_if_need(max_retries, 3, "Maximum retry count on test failure") + GdUnitSettings.create_property_if_need(enable_logging, true, "Enable test run logging") + GdUnitSettings.create_property_if_need(verbose_output, false, "Show verbose test output") + GdUnitSettings.create_property_if_need(log_path, "logs/", "Path to store log files") + GdUnitSettings.create_property_if_need(timeout_seconds, 30, "Connection timeout in seconds") + + var settings_settings := GdUnitSettings.list_settings("unit_test/settings") + assert_array(settings_settings)\ + .extractv(extr("name"), extr("type"), extr("value"), extr("default"), extr("help"), extr("value_set"))\ + .contains_exactly_in_any_order([ + tuple(report_errors, TYPE_BOOL, true, true, "Report errors as failures", PackedStringArray()), + tuple(report_warnings, TYPE_BOOL, false, false, "Report warnings as failures", PackedStringArray()), + tuple(max_retries, TYPE_INT, 3, 3, "Maximum retry count on test failure", PackedStringArray()), + ]) + var settings_network := GdUnitSettings.list_settings("unit_test/network") + assert_array(settings_network)\ + .extractv(extr("name"), extr("type"), extr("value"), extr("default"), extr("help"), extr("value_set"))\ + .contains_exactly_in_any_order([ + tuple(enable_logging, TYPE_BOOL, true, true, "Enable test run logging", PackedStringArray()), + tuple(verbose_output, TYPE_BOOL, false, false, "Show verbose test output", PackedStringArray()), + tuple(log_path, TYPE_STRING, "logs/", "logs/", "Path to store log files", PackedStringArray()), + tuple(timeout_seconds, TYPE_INT, 30, 30, "Connection timeout in seconds", PackedStringArray()), + ]) +#endregion + + +#region property info +func test_property_bool_info() -> void: + var property_name := "unit_test/property/update_notification_enabled" + GdUnitSettings.create_property_if_need(property_name, true, "Show notification when a new version is found") + + var property := GdUnitSettings.get_property(property_name) + assert_bool(property.value()).is_true() + assert_bool(property.default()).is_true() + assert_int(property.type()).is_equal(TYPE_BOOL) + assert_array(property.value_set()).is_empty() + assert_str(property.help()).is_equal("Show notification when a new version is found") + # verify Godot property API + var info := get_godot_property_info(property_name) + assert_dict(info)\ + .contains_key_value("type", TYPE_BOOL)\ + .contains_key_value("hint", PROPERTY_HINT_NONE)\ + .contains_key_value("hint_string", "") + assert_that(ProjectSettings.property_get_revert(property_name)).is_equal(true) + + +func test_property_int_info() -> void: + var property_name := "unit_test/property/server_timeout_minutes" + GdUnitSettings.create_property_if_need(property_name, 30, "Server connection timeout in minutes") + + var property := GdUnitSettings.get_property(property_name) + assert_int(property.value()).is_equal(30) + assert_int(property.default()).is_equal(30) + assert_int(property.type()).is_equal(TYPE_INT) + assert_array(property.value_set()).is_empty() + assert_str(property.help()).is_equal("Server connection timeout in minutes") + # verify Godot property API + var info := get_godot_property_info(property_name) + assert_dict(info)\ + .contains_key_value("type", TYPE_INT)\ + .contains_key_value("hint", PROPERTY_HINT_NONE)\ + .contains_key_value("hint_string", "") + assert_that(ProjectSettings.property_get_revert(property_name)).is_equal(30) + + +func test_property_string_info() -> void: + var property_name := "unit_test/property/test_lookup_folder" + GdUnitSettings.create_property_if_need(property_name, "test", "Subfolder where test suites are located") + + var property := GdUnitSettings.get_property(property_name) + assert_str(property.value()).is_equal("test") + assert_str(property.default()).is_equal("test") + assert_int(property.type()).is_equal(TYPE_STRING) + assert_array(property.value_set()).is_empty() + assert_str(property.help()).is_equal("Subfolder where test suites are located") + # verify Godot property API + var info := get_godot_property_info(property_name) + assert_dict(info)\ + .contains_key_value("type", TYPE_STRING)\ + .contains_key_value("hint", PROPERTY_HINT_NONE)\ + .contains_key_value("hint_string", "") + assert_that(ProjectSettings.property_get_revert(property_name)).is_equal("test") + + +func test_property_enum_info() -> void: + var property_name := "unit_test/property/naming_convention" + var value_set: PackedStringArray = GdUnitSettings.NAMING_CONVENTIONS.keys() + GdUnitSettings.create_property_if_need(property_name, GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT, "Naming convention for test suite generation", value_set) + + var property := GdUnitSettings.get_property(property_name) + assert_int(property.value()).is_equal(GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + assert_int(property.default()).is_equal(GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + assert_int(property.type()).is_equal(TYPE_INT) + assert_array(property.value_set()).is_equal(value_set) + assert_str(property.help()).is_equal("Naming convention for test suite generation") + # verify Godot property API + var info := get_godot_property_info(property_name) + assert_dict(info)\ + .contains_key_value("type", TYPE_INT)\ + .contains_key_value("hint", PROPERTY_HINT_ENUM)\ + .contains_key_value("hint_string", ",".join(value_set)) + assert_that(ProjectSettings.property_get_revert(property_name)).is_equal(GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) +#endregion + + +#region migrate_property +func test_migrate_property_change_key() -> void: + var old_property_X := "/category_patch/migrate_key/old_name" + var new_property_X := "/category_patch/migrate_key/new_name" + GdUnitSettings.create_property_if_need(old_property_X, "foo") + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_equal("foo") + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_null() + var old_property := GdUnitSettings.get_property(old_property_X) + + GdUnitSettings.migrate_property(old_property.name(),\ + new_property_X,\ + old_property.default(),\ + old_property.help()) + + var new_property := GdUnitSettings.get_property(new_property_X) + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_null() + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_equal("foo") + assert_object(new_property).is_not_equal(old_property) + assert_str(new_property.value()).is_equal(old_property.value()) + assert_array(new_property.value_set()).is_equal(old_property.value_set()) + assert_int(new_property.type()).is_equal(old_property.type()) + assert_str(new_property.default()).is_equal(old_property.default()) + assert_str(new_property.help()).is_equal(old_property.help()) + + +func test_migrate_property_change_value() -> void: + var old_property_X := "/category_patch/migrate_value/old_name" + var new_property_X := "/category_patch/migrate_value/new_name" + GdUnitSettings.create_property_if_need(old_property_X, "foo", "help to foo") + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_equal("foo") + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_null() + var old_property := GdUnitSettings.get_property(old_property_X) + + GdUnitSettings.migrate_property(old_property.name(),\ + new_property_X,\ + old_property.default(),\ + old_property.help(),\ + func(_value :Variant) -> String: return "bar") + + var new_property := GdUnitSettings.get_property(new_property_X) + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_null() + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_equal("bar") + assert_object(new_property).is_not_equal(old_property) + assert_str(new_property.value()).is_equal("bar") + assert_array(new_property.value_set()).is_equal(old_property.value_set()) + assert_int(new_property.type()).is_equal(old_property.type()) + assert_str(new_property.default()).is_equal(old_property.default()) + assert_str(new_property.help()).is_equal(old_property.help()) +#endregion + + +#region validate_is_inferred_declaration_enabled +func test_validate_is_inferred_declaration_enabled_when_disabled() -> void: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, IGNORE) + assert_result(GdUnitSettings.validate_is_inferred_declaration_enabled())\ + .is_success() + + +func test_validate_is_inferred_declaration_enabled_when_warning_and_addon_excluded() -> void: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, WARN) + if Engine.get_version_info().hex >= 0x40600: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, {"res://addons/gdUnit4": EXCLUDE}) + else: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_EXCLUDE_ADDONS, true) + assert_result(GdUnitSettings.validate_is_inferred_declaration_enabled())\ + .is_success() + + +func test_validate_is_inferred_declaration_enabled_when_warning_and_parent_dir_excluded() -> void: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, WARN) + if Engine.get_version_info().hex >= 0x40600: + # Godot's default: all plugins are excluded, covers gdUnit4 via parent path + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, {"res://addons": EXCLUDE}) + else: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_EXCLUDE_ADDONS, true) + assert_result(GdUnitSettings.validate_is_inferred_declaration_enabled())\ + .is_success() + + +func test_validate_is_inferred_declaration_enabled_when_warning_and_addon_excluded_but_parent_included() -> void: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, WARN) + if Engine.get_version_info().hex >= 0x40600: + # parent addons dir is included (warnings active) but gdUnit4 is specifically excluded + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, {"res://addons": INCLUDE, "res://addons/gdUnit4": EXCLUDE}) + else: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_EXCLUDE_ADDONS, true) + assert_result(GdUnitSettings.validate_is_inferred_declaration_enabled())\ + .is_success() + + +func test_validate_is_inferred_declaration_enabled_when_warning_and_only_parent_included() -> void: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, WARN) + var expected_message: String + if Engine.get_version_info().hex >= 0x40600: + # only "res://addons" is included (warnings active), gdUnit4 has no explicit exclusion + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, {"res://addons": INCLUDE}) + expected_message = """ + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude the addon (debug/gdscript/warnings/directory_rules) + """.dedent().strip_edges() + else: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_EXCLUDE_ADDONS, false) + expected_message = """ + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude addons (debug/gdscript/warnings/exclude_addons) + """.dedent().strip_edges() + assert_result(GdUnitSettings.validate_is_inferred_declaration_enabled())\ + .is_error()\ + .contains_message(expected_message) + + +func test_validate_is_inferred_declaration_enabled_when_warning_and_addon_not_excluded() -> void: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, WARN) + var expected_message: String + if Engine.get_version_info().hex >= 0x40600: + # default + gdUnit4 explicitly re-included: addon is not excluded from warnings + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, {"res://addons": EXCLUDE, "res://addons/gdUnit4": INCLUDE}) + expected_message = """ + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude the addon (debug/gdscript/warnings/directory_rules) + """.dedent().strip_edges() + else: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_EXCLUDE_ADDONS, false) + expected_message = """ + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude addons (debug/gdscript/warnings/exclude_addons) + """.dedent().strip_edges() + assert_result(GdUnitSettings.validate_is_inferred_declaration_enabled())\ + .is_error()\ + .contains_message(expected_message) + + +func test_validate_is_inferred_declaration_enabled_when_error_and_addon_not_excluded() -> void: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, ERROR) + var expected_message: String + if Engine.get_version_info().hex >= 0x40600: + # default + gdUnit4 explicitly re-included: addon is not excluded from warnings + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, {"res://addons": EXCLUDE, "res://addons/gdUnit4": INCLUDE}) + expected_message = """ + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude the addon (debug/gdscript/warnings/directory_rules) + """.dedent().strip_edges() + else: + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_EXCLUDE_ADDONS, false) + expected_message = """ + GdUnit4: 'inferred_declaration' is set to Warning/Error! + GdUnit4 is not 'inferred_declaration' safe, you have to exclude addons (debug/gdscript/warnings/exclude_addons) + """.dedent().strip_edges() + assert_result(GdUnitSettings.validate_is_inferred_declaration_enabled())\ + .is_error()\ + .contains_message(expected_message) +#endregion diff --git a/addons/gdUnit4/test/core/GdUnitSettingsTest.gd.uid b/addons/gdUnit4/test/core/GdUnitSettingsTest.gd.uid new file mode 100644 index 0000000..2dac210 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSettingsTest.gd.uid @@ -0,0 +1 @@ +uid://d1mj2alj6pdrf diff --git a/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd b/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd new file mode 100644 index 0000000..986e811 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd @@ -0,0 +1,46 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitSignalAwaiterTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd' + + +class Monster extends Node: + + signal move(value :float) + signal slide(value :float, x :int, z :int) + + var _pos :float = 0.0 + + func _process(_delta :float) -> void: + _pos += 0.2 + emit_signal(move.get_name(), _pos) + emit_signal(slide.get_name(), _pos, 1 , 2) + + +func test_on_signal_with_single_arg() -> void: + var monster :Monster = auto_free(Monster.new()) + add_child(monster) + var signal_arg :Variant = await await_signal_on(monster, "move", [1.0]) + assert_float(signal_arg).is_equal(1.0) + remove_child(monster) + + +func test_on_signal_with_many_args() -> void: + var monster :Monster = auto_free(Monster.new()) + add_child(monster) + var signal_args :Variant = await await_signal_on(monster, "slide", [1.0, 1, 2]) + assert_array(signal_args).is_equal([1.0, 1, 2]) + remove_child(monster) + + +func test_on_signal_fail() -> void: + var monster :Monster = auto_free(Monster.new()) + add_child(monster) + ( + await assert_failure_await( func x() -> void: await await_signal_on(monster, "move", [4.0])) + ).has_message("await_signal_on(move, [$v0]) timed out after 2000ms".replace("$v0", str(4.0))) + remove_child(monster) diff --git a/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd.uid b/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd.uid new file mode 100644 index 0000000..14f8b7f --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd.uid @@ -0,0 +1 @@ +uid://crj4r80k2egba diff --git a/addons/gdUnit4/test/core/GdUnitSignalMonitorTest.gd b/addons/gdUnit4/test/core/GdUnitSignalMonitorTest.gd new file mode 100644 index 0000000..a3ad389 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSignalMonitorTest.gd @@ -0,0 +1,56 @@ +extends GdUnitTestSuite +@warning_ignore_start("redundant_await") + +# This test setup uses two monitors, which are created for each new test. +# The monitors must be reinitialized for each run, otherwise signals from previous runs will be detected incorrectly. +# See https://github.com/MikeSchulze/gdUnit4/issues/1002 +func before_test() -> void: + monitor_signals(o2.o1, false) + monitor_signals(o2, false) + + +# This test runs first and emits on object o1 and o2 the signals and is cauched by both monitors +func test_monitor_obj1() -> void: + o2.o1.emit() + await assert_signal(o2.o1).is_emitted("s1") + await assert_signal(o2).is_emitted("s2") + assert_int(o2.x).is_equal(2) + + +# This test runs after `test_monitor_obj1` and must fail because the signal is not emitted yet +func test_monitor_obj2() -> void: + var result := await assert_failure_await(await func() -> void: + await assert_signal(o2).wait_until(50).is_emitted("s2") + ) + result.has_message("Expecting emit signal: 's2()' but timed out after 50ms") + + +func test_monitor_obj2_success() -> void: + o2._on_s1() + await assert_signal(o2).wait_until(500).is_emitted("s2") + +class C1: + signal s1 + + func emit() -> void: + s1.emit() + +class C2: + signal s2 + var x := 0 + var o1: C1 + + func _init() -> void: + o1 = C1.new() + o1.s1.connect(_on_s1) + x = 1 + + func _on_s1() -> void: + s2.emit() + x = 2 + +var o2: C2 + + +func before() -> void: + o2 = C2.new() diff --git a/addons/gdUnit4/test/core/GdUnitSignalMonitorTest.gd.uid b/addons/gdUnit4/test/core/GdUnitSignalMonitorTest.gd.uid new file mode 100644 index 0000000..b1e2817 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSignalMonitorTest.gd.uid @@ -0,0 +1 @@ +uid://cbhecn6jfh28t diff --git a/addons/gdUnit4/test/core/GdUnitSingletonTest.gd b/addons/gdUnit4/test/core/GdUnitSingletonTest.gd new file mode 100644 index 0000000..54fcbed --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSingletonTest.gd @@ -0,0 +1,37 @@ +class_name GdUnitSingletonTest +extends GdUnitTestSuite + + +static var _instance_called := 0 + +class ExampleSingletonImplementaion extends Object: + + static func instance() -> ExampleSingletonImplementaion: + return GdUnitSingleton.instance("ExampleSingletonImplementaion", func() -> ExampleSingletonImplementaion: + GdUnitSingletonTest._instance_called += 1 + return ExampleSingletonImplementaion.new() + ) + +func test_instance() -> void: + var n :Variant = GdUnitSingleton.instance("singelton_test", func() -> Node: return Node.new() ) + assert_object(n).is_instanceof(Node) + assert_bool(is_instance_valid(n)).is_true() + + # free the singleton + GdUnitSingleton.unregister("singelton_test") + assert_bool(is_instance_valid(n)).is_false() + + +func test_instance_implementaion() -> void: + assert_bool(Engine.has_meta("ExampleSingletonImplementaion")).is_false() + + var instance1 := ExampleSingletonImplementaion.instance() + var instance2 := ExampleSingletonImplementaion.instance() + + assert_bool(Engine.has_meta("ExampleSingletonImplementaion")).is_true() + assert_object(instance1).is_same(instance2) + assert_int(_instance_called).is_equal(1) + + # finally free it + GdUnitSingleton.unregister("ExampleSingletonImplementaion") + assert_bool(Engine.has_meta("ExampleSingletonImplementaion")).is_false() diff --git a/addons/gdUnit4/test/core/GdUnitSingletonTest.gd.uid b/addons/gdUnit4/test/core/GdUnitSingletonTest.gd.uid new file mode 100644 index 0000000..bdae277 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSingletonTest.gd.uid @@ -0,0 +1 @@ +uid://bgqgbnkf7e4kd diff --git a/addons/gdUnit4/test/core/GdUnitStackTraceElementTest.gd b/addons/gdUnit4/test/core/GdUnitStackTraceElementTest.gd new file mode 100644 index 0000000..c0f124c --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitStackTraceElementTest.gd @@ -0,0 +1,13 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitStackTraceElement.gd' + + +func test_of_creates_element_from_dict() -> void: + var data := {"source": "res://test.gd", "line": 42, "function": "my_func"} + assert_that(GdUnitStackTraceElement.of(data)) \ + .is_equal(GdUnitStackTraceElement.new("res://test.gd", 42, "my_func")) diff --git a/addons/gdUnit4/test/core/GdUnitStackTraceElementTest.gd.uid b/addons/gdUnit4/test/core/GdUnitStackTraceElementTest.gd.uid new file mode 100644 index 0000000..693132f --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitStackTraceElementTest.gd.uid @@ -0,0 +1 @@ +uid://dkhiks30bm8s3 diff --git a/addons/gdUnit4/test/core/GdUnitStackTraceTest.gd b/addons/gdUnit4/test/core/GdUnitStackTraceTest.gd new file mode 100644 index 0000000..3ec42a3 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitStackTraceTest.gd @@ -0,0 +1,176 @@ +class_name GdUnitStackTraceTest +extends GdUnitTestSuite + +const __source = 'res://addons/gdUnit4/src/core/GdUnitStackTrace.gd' + + +#region helpers +func capture_stack() -> GdUnitStackTrace: + return GdUnitStackTrace.new() + + +func capture_stack_1() -> GdUnitStackTrace: + return capture_stack() + + +func capture_stack_2() -> GdUnitStackTrace: + return capture_stack_1() + + +func capture_stack_3() -> GdUnitStackTrace: + return capture_stack_2() + + +func capture_stack_4() -> GdUnitStackTrace: + return capture_stack_3() + + +class InnerClass: + + static func capture_stack() -> GdUnitStackTrace: + return GdUnitStackTrace.new() + +#endregion + + +#region tests + +func test_captures_stack_depth_0() -> void: + var trace := GdUnitStackTrace.new() + assert_int(trace.get_line_number()).is_equal(39) + assert_str(trace.print_stack_trace())\ + .is_equal("\tat 'test_captures_stack_depth_0' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:39\n") + + +func test_captures_stack_depth_1() -> void: + var trace := capture_stack() + assert_int(trace.get_line_number()).is_equal(9) + assert_str(trace.print_stack_trace())\ + .is_equal(""" + at 'capture_stack' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:9 + at 'test_captures_stack_depth_1' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:46 + """.dedent().indent("\t").trim_prefix("\n")) + + +func test_captures_stack_depth_2() -> void: + var trace := capture_stack_1() + assert_int(trace.get_line_number()).is_equal(9) + assert_str(trace.print_stack_trace())\ + .is_equal(""" + at 'capture_stack' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:9 + at 'capture_stack_1' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:13 + at 'test_captures_stack_depth_2' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:56 + """.dedent().indent("\t").trim_prefix("\n")) + + +func test_captures_stack_depth_3() -> void: + var trace := capture_stack_2() + assert_int(trace.get_line_number()).is_equal(9) + assert_str(trace.print_stack_trace())\ + .is_equal(""" + at 'capture_stack' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:9 + at 'capture_stack_1' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:13 + at 'capture_stack_2' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:17 + at 'test_captures_stack_depth_3' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:67 + """.dedent().indent("\t").trim_prefix("\n")) + + +func test_captures_stack_depth_4() -> void: + var trace := capture_stack_3() + assert_int(trace.get_line_number()).is_equal(9) + assert_str(trace.print_stack_trace())\ + .is_equal(""" + at 'capture_stack' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:9 + at 'capture_stack_1' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:13 + at 'capture_stack_2' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:17 + at 'capture_stack_3' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:21 + at 'test_captures_stack_depth_4' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:79 + """.dedent().indent("\t").trim_prefix("\n")) + + +func test_captures_stack_depth_5() -> void: + var trace := capture_stack_4() + assert_int(trace.get_line_number()).is_equal(9) + assert_str(trace.print_stack_trace())\ + .is_equal(""" + at 'capture_stack' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:9 + at 'capture_stack_1' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:13 + at 'capture_stack_2' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:17 + at 'capture_stack_3' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:21 + at 'capture_stack_4' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:25 + at 'test_captures_stack_depth_5' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:92 + """.dedent().indent("\t").trim_prefix("\n")) + + +func test_mock_frames_are_filtered_from_stack_trace() -> void: + var mock_node: Variant = mock(Node) + assert_failure(func verify_call() -> void: + @warning_ignore("unsafe_method_access") + verify(mock_node, 1).set_process(true)\ + )\ + .has_stack_trace([ + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd", 109, "verify_call"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd", 107, "test_mock_frames_are_filtered_from_stack_trace"), + ]) + + +func test_spy_frames_are_filtered_from_stack_trace() -> void: + var instance: Node = auto_free(Node.new()) + var spy_node: Variant = spy(instance) + assert_failure(func verify_call() -> void: + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).set_process(true)\ + )\ + .has_stack_trace([ + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd", 122, "verify_call"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd", 120, "test_spy_frames_are_filtered_from_stack_trace"), + ]) + + +func test_inner_class_frames_in_stack_trace() -> void: + var trace := InnerClass.capture_stack() + assert_int(trace.get_line_number()).is_equal(31) + assert_str(trace.print_stack_trace())\ + .is_equal(""" + at 'capture_stack' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:31 + at 'test_inner_class_frames_in_stack_trace' in res://addons/gdUnit4/test/core/GdUnitStackTraceTest.gd:131 + """.dedent().indent("\t").trim_prefix("\n")) + +#endregion + + +#region serialize / deserialize + +func test_serialize_empty_trace_returns_empty_json_array() -> void: + var trace := GdUnitStackTrace.of([]) + assert_str(trace.serialize()).is_equal("[]") + + +func test_serialize_returns_json_string() -> void: + var trace := GdUnitStackTrace.of([ + GdUnitStackTraceElement.new("res://test.gd", 10, "func_a"), + GdUnitStackTraceElement.new("res://test.gd", 20, "func_b"), + ]) + var json := trace.serialize() + assert_str(json)\ + .is_not_empty()\ + .is_equal('[{"function":"func_a","line":10,"source":"res://test.gd"},{"function":"func_b","line":20,"source":"res://test.gd"}]') + + +func test_deserialize_reconstructs_frames() -> void: + var json := '[{"source":"res://test.gd","line":10,"function":"func_a"},{"source":"res://test.gd","line":20,"function":"func_b"}]' + var trace := GdUnitStackTrace.deserialize(json) + assert_that(trace).is_equal(GdUnitStackTrace.of([ + GdUnitStackTraceElement.new("res://test.gd", 10, "func_a"), + GdUnitStackTraceElement.new("res://test.gd", 20, "func_b"), + ])) + + +func test_serialize_deserialize_round_trip() -> void: + var original := GdUnitStackTrace.of([ + GdUnitStackTraceElement.new("res://my_test.gd", 42, "check_value"), + GdUnitStackTraceElement.new("res://my_test.gd", 55, "run_suite"), + ]) + var restored := GdUnitStackTrace.deserialize(original.serialize()) + assert_that(restored).is_equal(original) +#endregion diff --git a/addons/gdUnit4/test/core/GdUnitStackTraceTest.gd.uid b/addons/gdUnit4/test/core/GdUnitStackTraceTest.gd.uid new file mode 100644 index 0000000..d1b8fb1 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitStackTraceTest.gd.uid @@ -0,0 +1 @@ +uid://b54un2thxkq8w diff --git a/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd new file mode 100644 index 0000000..62c8029 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd @@ -0,0 +1,72 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitTestSuiteBuilderTest +extends GdUnitTestSuite + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd' + +var _example_source_gd :String + + +func before() -> void: + clean_temp_dir() + + +func before_test() -> void: + var temp := create_temp_dir("examples") + var result := GdUnitFileAccess.copy_file("res://addons/gdUnit4/test/core/resources/sources/test_person.gd", temp) + assert_result(result).is_success() + _example_source_gd = result.value_as_string() + + +func after_test() -> void: + clean_temp_dir() + + +func assert_tests(test_suite :Script) -> GdUnitArrayAssert: + # needs to be reload to get fresh method list + test_suite.reload() + var methods := test_suite.get_script_method_list() + var test_cases := Array() + for method in methods: + @warning_ignore("unsafe_method_access") + if method.name.begins_with("test_"): + test_cases.append(method.name) + return assert_array(test_cases) + + +func test_create_gd_success() -> void: + var source: GDScript = load(_example_source_gd) + + # create initial test suite based checked function selected by line 9 + var result := GdUnitTestSuiteBuilder.create(source, 9) + + assert_result(result).is_success() + var info: Dictionary = result.value() + assert_str(info.get("path")).is_equal("user://tmp/test/examples/test_person_test.gd") + assert_int(info.get("line")).is_equal(11) + @warning_ignore("unsafe_cast") + assert_tests(load(info.get("path") as String) as Script).contains_exactly(["test_first_name"]) + + # create additional test checked existing suite based checked function selected by line 15 + result = GdUnitTestSuiteBuilder.create(source, 15) + + assert_result(result).is_success() + info = result.value() + assert_str(info.get("path")).is_equal("user://tmp/test/examples/test_person_test.gd") + assert_int(info.get("line")).is_equal(16) + @warning_ignore("unsafe_cast") + assert_tests(load(info.get("path") as String) as Script).contains_exactly_in_any_order(["test_first_name", "test_fully_name"]) + + +func test_create_gd_fail() -> void: + var source: GDScript = load(_example_source_gd) + + # attempt to create an initial test suite based checked the function selected in line 8, which has no function definition + var result := GdUnitTestSuiteBuilder.create(source, 8) + assert_result(result).is_error().contains_message("No function found at line: 8.") diff --git a/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd.uid b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd.uid new file mode 100644 index 0000000..2143b7b --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd.uid @@ -0,0 +1 @@ +uid://c5jtbp46wo0eh diff --git a/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd b/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd new file mode 100644 index 0000000..4003ed9 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd @@ -0,0 +1,311 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name TestSuiteScannerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd' + +func before_test() -> void: + ProjectSettings.set_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + clean_temp_dir() + + +func after() -> void: + clean_temp_dir() + + +func resolve_path(source_file :String) -> String: + return GdUnitTestSuiteScanner.resolve_test_suite_path(source_file, "_test_") + + +func test_resolve_test_suite_path_project() -> void: + # if no `src` folder found use test folder as root + assert_str(resolve_path("res://foo.gd")).is_equal("res://_test_/foo_test.gd") + assert_str(resolve_path("res://project_name/module/foo.gd")).is_equal("res://_test_/project_name/module/foo_test.gd") + # otherwise build relative to 'src' + assert_str(resolve_path("res://src/foo.gd")).is_equal("res://_test_/foo_test.gd") + assert_str(resolve_path("res://project_name/src/foo.gd")).is_equal("res://project_name/_test_/foo_test.gd") + assert_str(resolve_path("res://project_name/src/module/foo.gd")).is_equal("res://project_name/_test_/module/foo_test.gd") + + +func test_resolve_test_suite_path_plugins() -> void: + assert_str(resolve_path("res://addons/plugin_a/foo.gd")).is_equal("res://addons/plugin_a/_test_/foo_test.gd") + assert_str(resolve_path("res://addons/plugin_a/src/foo.gd")).is_equal("res://addons/plugin_a/_test_/foo_test.gd") + + +func test_resolve_test_suite_path__no_test_root() -> void: + # from a project path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/models/events/ModelChangedEvent.gd", ""))\ + .is_equal("res://project/src/models/events/ModelChangedEventTest.gd") + # from a plugin path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/src/models/events/ModelChangedEvent.gd", ""))\ + .is_equal("res://addons/MyPlugin/src/models/events/ModelChangedEventTest.gd") + # located in user path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/src/models/events/ModelChangedEvent.gd", ""))\ + .is_equal("user://project/src/models/events/ModelChangedEventTest.gd") + + +func test_resolve_test_suite_path__path_contains_src_folder() -> void: + # from a project path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://project/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://project/custom_test/models/events/ModelChangedEventTest.gd") + # from a plugin path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/src/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://addons/MyPlugin/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/src/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://addons/MyPlugin/custom_test/models/events/ModelChangedEventTest.gd") + # located in user path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/src/models/events/ModelChangedEvent.gd"))\ + .is_equal("user://project/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/src/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("user://project/custom_test/models/events/ModelChangedEventTest.gd") + + +func test_resolve_test_suite_path__path_not_contains_src_folder() -> void: + # from a project path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://test/project/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://custom_test/project/models/events/ModelChangedEventTest.gd") + # from a plugin path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://addons/MyPlugin/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://addons/MyPlugin/custom_test/models/events/ModelChangedEventTest.gd") + # located in user path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/models/events/ModelChangedEvent.gd"))\ + .is_equal("user://test/project/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("user://custom_test/project/models/events/ModelChangedEventTest.gd") + + +func test_test_suite_exists() -> void: + var path_exists := "res://addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd" + var path_not_exists := "res://addons/gdUnit4/test/resources/core/FamilyTest.gd" + assert_bool(GdUnitTestSuiteScanner.test_suite_exists(path_exists)).is_true() + assert_bool(GdUnitTestSuiteScanner.test_suite_exists(path_not_exists)).is_false() + + +func test_test_case_exists() -> void: + var test_suite_path := "res://addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd" + assert_bool(GdUnitTestSuiteScanner.test_case_exists(test_suite_path, "name")).is_true() + assert_bool(GdUnitTestSuiteScanner.test_case_exists(test_suite_path, "last_name")).is_false() + + +func test_create_test_suite_pascal_case_path() -> void: + var temp_dir := create_temp_dir("TestSuiteScannerTest") + # checked source with class_name is set + var source_path := "res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd" + var suite_path := temp_dir + "/test/MyClassTest1.gd" + var result := GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_bool(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name PascalCaseWithClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source: String = '%s'" % source_path, + ""]) + # checked source with class_name is NOT set + source_path = "res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd" + suite_path = temp_dir + "/test/MyClassTest2.gd" + result = GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_bool(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name PascalCaseWithoutClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source: String = '%s'" % source_path, + ""]) + + +func test_create_test_suite_snake_case_path() -> void: + var temp_dir := create_temp_dir("TestSuiteScannerTest") + # checked source with class_name is set + var source_path :="res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd" + var suite_path := temp_dir + "/test/my_class_test1.gd" + var result := GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_bool(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name SnakeCaseWithClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source: String = '%s'" % source_path, + ""]) + # checked source with class_name is NOT set + source_path ="res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd" + suite_path = temp_dir + "/test/my_class_test2.gd" + result = GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_bool(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name SnakeCaseWithoutClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source: String = '%s'" % source_path, + ""]) + + +func test_create_test_case() -> void: + # store test class checked temp dir + var tmp_path := create_temp_dir("TestSuiteScannerTest") + var source_path := "res://addons/gdUnit4/test/resources/core/Person.gd" + # generate new test suite with test 'test_last_name()' + var test_suite_path := tmp_path + "/test/PersonTest.gd" + var result := GdUnitTestSuiteScanner.create_test_case(test_suite_path, "last_name", source_path) + assert_bool(result.is_success()).is_true() + var info :Dictionary = result.value() + assert_int(info.get("line")).is_equal(11) + assert_file(info.get("path")).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name PersonTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source: String = '%s'" % source_path, + "", + "", + "func test_last_name() -> void:", + " # remove this line and complete your test", + " assert_not_yet_implemented()", + ""]) + # try to add again + result = GdUnitTestSuiteScanner.create_test_case(test_suite_path, "last_name", source_path) + assert_bool(result.is_success()).is_true() + assert_that(result.value()).is_equal({"line" : 16, "path": test_suite_path}) + + +# https://github.com/MikeSchulze/gdUnit4/issues/25 +func test_build_test_suite_path() -> void: + # checked project root + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://new_script.gd")).is_equal("res://test/new_script_test.gd") + + # checked project without src folder + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://foo/bar/new_script.gd")).is_equal("res://test/foo/bar/new_script_test.gd") + + # project code structured by 'src' + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://src/new_script.gd")).is_equal("res://test/new_script_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://src/foo/bar/new_script.gd")).is_equal("res://test/foo/bar/new_script_test.gd") + # folder name contains 'src' in name + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://foo/srcare/new_script.gd")).is_equal("res://test/foo/srcare/new_script_test.gd") + + # checked plugins without src folder + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/plugin/foo/bar/new_script.gd")).is_equal("res://addons/plugin/test/foo/bar/new_script_test.gd") + # plugin code structured by 'src' + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/plugin/src/foo/bar/new_script.gd")).is_equal("res://addons/plugin/test/foo/bar/new_script_test.gd") + + # checked user temp folder + var tmp_path := create_temp_dir("projectX/entity") + var source_path := tmp_path + "/Person.gd" + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path(source_path)).is_equal("user://tmp/test/projectX/entity/PersonTest.gd") + + +func test_scan_by_inheritance_class_name() -> void: + var scanner :GdUnitTestSuiteScanner = GdUnitTestSuiteScanner.new() + var test_suites := scanner.scan("res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/") + + assert_array(test_suites).has_size(3) + # sort by names + assert_array(test_suites).extract("resource_path")\ + .contains_exactly_in_any_order([ + "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd", + "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd", + "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd"]) + + +func test_get_test_case_line_number() -> void: + assert_int(GdUnitTestSuiteScanner.get_test_case_line_number("res://addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd", "get_test_case_line_number")).is_equal(255) + assert_int(GdUnitTestSuiteScanner.get_test_case_line_number("res://addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd", "unknown")).is_equal(-1) + + +func test__to_naming_convention() -> void: + ProjectSettings.set_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + assert_str(GdUnitTestSuiteScanner._to_naming_convention("MyClass")).is_equal("MyClassTest") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("my_class")).is_equal("my_class_test") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("myclass")).is_equal("myclass_test") + + ProjectSettings.set_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.SNAKE_CASE) + assert_str(GdUnitTestSuiteScanner._to_naming_convention("MyClass")).is_equal("my_class_test") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("my_class")).is_equal("my_class_test") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("myclass")).is_equal("myclass_test") + + ProjectSettings.set_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.PASCAL_CASE) + assert_str(GdUnitTestSuiteScanner._to_naming_convention("MyClass")).is_equal("MyClassTest") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("my_class")).is_equal("MyClassTest") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("myclass")).is_equal("MyclassTest") + + +func test_is_script_format_supported() -> void: + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.gd")).is_true() + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.gdns")).is_false() + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.vs")).is_false() + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.tres")).is_false() + + +func test_resolve_test_suite_path() -> void: + # forcing the use of a test folder next to the source folder + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/myclass.gd", "test")).is_equal("res://project/test/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/MyClass.gd", "test")).is_equal("res://project/test/folder/MyClassTest.gd") + # forcing to use source directory to create the test + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/myclass.gd", "")).is_equal("res://project/src/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/MyClass.gd", "")).is_equal("res://project/src/folder/MyClassTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/myclass.gd", "/")).is_equal("res://project/src/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/MyClass.gd", "/")).is_equal("res://project/src/folder/MyClassTest.gd") + + +func test_resolve_test_suite_path_with_src_folders() -> void: + # forcing the use of a test folder next + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/myclass.gd", "test")).is_equal("res://test/project/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/MyClass.gd", "test")).is_equal("res://test/project/folder/MyClassTest.gd") + # forcing to use source directory to create the test + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/myclass.gd", "")).is_equal("res://project/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/MyClass.gd", "")).is_equal("res://project/folder/MyClassTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/myclass.gd", "/")).is_equal("res://project/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/MyClass.gd", "/")).is_equal("res://project/folder/MyClassTest.gd") + + +func test_scan_test_suite_exclude_non_test_suites() -> void: + var scanner :GdUnitTestSuiteScanner = GdUnitTestSuiteScanner.new() + var test_suites := scanner.scan("res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/") + + # we expect the scanner do not break on scanning plugin classes + assert_array(test_suites).is_empty() diff --git a/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd.uid b/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd.uid new file mode 100644 index 0000000..0d8b70e --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd.uid @@ -0,0 +1 @@ +uid://tmddcinay2yk diff --git a/addons/gdUnit4/test/core/GdUnitToolsTest.gd b/addons/gdUnit4/test/core/GdUnitToolsTest.gd new file mode 100644 index 0000000..f20409f --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitToolsTest.gd @@ -0,0 +1,49 @@ +# GdUnit generated TestSuite +class_name GdUnitToolsTest +extends GdUnitTestSuite + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitTools.gd' + + +class InnerTestNodeClass extends Node: + pass + +class InnerTestRefCountedClass extends RefCounted: + pass + + +func test_free_instance() -> void: + # on valid instances + assert_bool(await GdUnitTools.free_instance(RefCounted.new())).is_true() + assert_bool(await GdUnitTools.free_instance(Node.new())).is_true() + assert_bool(await GdUnitTools.free_instance(JavaClass.new())).is_true() + assert_bool(await GdUnitTools.free_instance(InnerTestNodeClass.new())).is_true() + assert_bool(await GdUnitTools.free_instance(InnerTestRefCountedClass.new())).is_true() + + # on invalid instances + assert_bool(await GdUnitTools.free_instance(null)).is_false() + assert_bool(await GdUnitTools.free_instance(RefCounted)).is_false() + + # on already freed instances + var node := Node.new() + node.free() + assert_bool(await GdUnitTools.free_instance(node)).is_false() + + +func test_richtext_normalize() -> void: + assert_that(GdUnitTools.richtext_normalize("")).is_equal("") + assert_that(GdUnitTools.richtext_normalize("This is a Color Message")).is_equal("This is a Color Message") + + var message := """ + [color=green]line [/color][color=aqua]11:[/color] [color=#CD5C5C]Expecting:[/color] + must be empty but was + '[color=#1E90FF]after[/color]' + """ + assert_that(GdUnitTools.richtext_normalize(message)).is_equal(""" + line 11: Expecting: + must be empty but was + 'after' + """) diff --git a/addons/gdUnit4/test/core/GdUnitToolsTest.gd.uid b/addons/gdUnit4/test/core/GdUnitToolsTest.gd.uid new file mode 100644 index 0000000..d6832e4 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitToolsTest.gd.uid @@ -0,0 +1 @@ +uid://dfadbkq2qcevc diff --git a/addons/gdUnit4/test/core/InputEventTestScene.tscn b/addons/gdUnit4/test/core/InputEventTestScene.tscn new file mode 100644 index 0000000..e231a8b --- /dev/null +++ b/addons/gdUnit4/test/core/InputEventTestScene.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=2 format=3 uid="uid://7fcklvxjr88k"] + +[sub_resource type="GDScript" id="GDScript_h62nu"] +script/source = "extends Control + + +func _input(event: InputEvent) -> void: + prints(\">>\", GdAssertMessages.input_event_as_text(event)) + get_viewport().set_input_as_handled() +" + +[node name="KeyTest" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = SubResource("GDScript_h62nu") diff --git a/addons/gdUnit4/test/core/LocalTimeTest.gd b/addons/gdUnit4/test/core/LocalTimeTest.gd new file mode 100644 index 0000000..9d36ba2 --- /dev/null +++ b/addons/gdUnit4/test/core/LocalTimeTest.gd @@ -0,0 +1,72 @@ +# GdUnit generated TestSuite +class_name LocalTimeTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/LocalTime.gd' + + +func test_time_constants() -> void: + assert_int(LocalTime.MILLIS_PER_HOUR).is_equal(1000*60*60) + assert_int(LocalTime.MILLIS_PER_MINUTE).is_equal(1000*60) + assert_int(LocalTime.MILLIS_PER_SECOND).is_equal(1000) + assert_int(LocalTime.HOURS_PER_DAY).is_equal(24) + assert_int(LocalTime.MINUTES_PER_HOUR).is_equal(60) + assert_int(LocalTime.SECONDS_PER_MINUTE).is_equal(60) + + +func test_now() -> void: + var current := Time.get_datetime_dict_from_system(true) + var local_time := LocalTime.now() + assert_int(local_time.hour()).is_equal(current.get("hour")) + assert_int(local_time.minute()).is_equal(current.get("minute")) + assert_int(local_time.second()).is_equal(current.get("second")) + # Time.get_datetime_dict_from_system() does not provide milliseconds + #assert_that(local_time.millis()).is_equal(0) + + +@warning_ignore("integer_division") +func test_of_unix_time() -> void: + var time := LocalTime._get_system_time_msecs() + var local_time := LocalTime.of_unix_time(time) + @warning_ignore("integer_division") + assert_int(local_time.hour()).is_equal((time / LocalTime.MILLIS_PER_HOUR) % 24) + @warning_ignore("integer_division") + assert_int(local_time.minute()).is_equal((time / LocalTime.MILLIS_PER_MINUTE) % 60) + @warning_ignore("integer_division") + assert_int(local_time.second()).is_equal((time / LocalTime.MILLIS_PER_SECOND) % 60) + assert_int(local_time.millis()).is_equal(time % 1000) + + +func test_to_string() -> void: + assert_str(LocalTime.local_time(10, 12, 22, 333)._to_string()).is_equal("10:12:22.333") + assert_str(LocalTime.local_time(23, 59, 59, 999)._to_string()).is_equal("23:59:59.999") + assert_str(LocalTime.local_time( 0, 0, 0, 000)._to_string()).is_equal("00:00:00.000") + assert_str(LocalTime.local_time( 2, 4, 3, 10)._to_string()).is_equal("02:04:03.010") + + +func test_plus_seconds() -> void: + var time := LocalTime.local_time(10, 12, 22, 333) + assert_str(time.plus(LocalTime.TimeUnit.SECOND, 10)._to_string()).is_equal("10:12:32.333") + assert_str(time.plus(LocalTime.TimeUnit.SECOND, 27)._to_string()).is_equal("10:12:59.333") + assert_str(time.plus(LocalTime.TimeUnit.SECOND, 1)._to_string()).is_equal("10:13:00.333") + + # test overflow + var time2 := LocalTime.local_time(10, 59, 59, 333) + var start_time := time2._time + for iteration in 10000: + var t := LocalTime.of_unix_time(start_time) + var seconds:int = randi_range(0, 1000) + t.plus(LocalTime.TimeUnit.SECOND, seconds) + var expected := LocalTime.of_unix_time(start_time + (seconds * LocalTime.MILLIS_PER_SECOND)) + assert_str(t._to_string()).is_equal(expected._to_string()) + + +func test_elapsed() -> void: + assert_str(LocalTime.elapsed(10)).is_equal("10ms") + assert_str(LocalTime.elapsed(201)).is_equal("201ms") + assert_str(LocalTime.elapsed(999)).is_equal("999ms") + assert_str(LocalTime.elapsed(1000)).is_equal("1s 0ms") + assert_str(LocalTime.elapsed(2000)).is_equal("2s 0ms") + assert_str(LocalTime.elapsed(3040)).is_equal("3s 40ms") + assert_str(LocalTime.elapsed(LocalTime.MILLIS_PER_MINUTE * 6 + 3040)).is_equal("6min 3s 40ms") diff --git a/addons/gdUnit4/test/core/LocalTimeTest.gd.uid b/addons/gdUnit4/test/core/LocalTimeTest.gd.uid new file mode 100644 index 0000000..fa67ac8 --- /dev/null +++ b/addons/gdUnit4/test/core/LocalTimeTest.gd.uid @@ -0,0 +1 @@ +uid://c811gw3um42cx diff --git a/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd b/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd new file mode 100644 index 0000000..a9dbb4c --- /dev/null +++ b/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd @@ -0,0 +1,321 @@ +#warning-ignore-all:unused_argument +class_name ParameterizedTestCaseTest +extends GdUnitTestSuite + +var _collected_tests: Dictionary[String, Array] = {} +var _expected_tests := { + "test_parameterized_bool_value" : [ + [0, false], + [1, true] + ], + "test_parameterized_int_values" : [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] + ], + "test_parameterized_float_values" : [ + [2.2, 2.2, 4.4], + [2.2, 2.3, 4.5], + [3.3, 2.2, 5.5] + ], + "test_parameterized_string_values" : [ + ["2.2", "2.2", "2.22.2"], + ["foo", "bar", "foobar"], + ["a", "b", "ab"] + ], + "test_parameterized_Vector2_values" : [ + [Vector2.ONE, Vector2.ONE, Vector2(2, 2)], + [Vector2.LEFT, Vector2.RIGHT, Vector2.ZERO], + [Vector2.ZERO, Vector2.LEFT, Vector2.LEFT] + ], + "test_parameterized_Vector3_values" : [ + [Vector3.ONE, Vector3.ONE, Vector3(2, 2, 2)], + [Vector3.LEFT, Vector3.RIGHT, Vector3.ZERO], + [Vector3.ZERO, Vector3.LEFT, Vector3.LEFT] + ], + "test_parameterized_obj_values" : [ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"] + ], + "test_parameterized_dict_values" : [ + [{"key_a":"value_a"}, '{"key_a":"value_a"}'], + [{"key_b":"value_b"}, '{"key_b":"value_b"}'] + ], + "test_parameterized_untyped_array" : [ + [[42]] + ], + "test_parameterized_typed_array" : [ + [[42]] + ], + "test_with_dynamic_paramater_resolving" : [ + ["test_a"], + ["test_b"], + ["test_c"], + ["test_d"] + ], + "test_with_dynamic_paramater_resolving2" : [ + ["test_a"], + ["test_b"], + ["test_c"] + ], + "test_with_extern_parameter_set" : [ + ["test_a"], + ["test_b"], + ["test_c"] + ], + "test_with_extern_const_parameter_set" : [ + ["aa"], + ["bb"] + ] +} + + +var _test_node_before: Node +var _test_node_before_test: Node + + +func before() -> void: + _test_node_before = auto_free(SubViewport.new()) + + +func before_test() -> void: + _test_node_before_test = auto_free(SubViewport.new()) + + +func after() -> void: + for test_name: String in _expected_tests.keys(): + if _collected_tests.has(test_name): + var current_values: Variant = _collected_tests[test_name] + var expected_values: Variant = _expected_tests[test_name] + assert_that(current_values)\ + .override_failure_message("Expecting '%s' called with parameters:\n %s\n but was\n %s" % [test_name, expected_values, current_values])\ + .is_equal(expected_values) + else: + fail("Missing test '%s' executed!" % test_name) + + +func collect_test_call(test_name: String, values: Array) -> void: + if not _collected_tests.has(test_name): + _collected_tests[test_name] = Array() + _collected_tests[test_name].append(values) + + +func test_parameterized_bool_value(a: int, expected: bool, _test_parameters := [ + [0, false], + [1, true]]) -> void: + collect_test_call("test_parameterized_bool_value", [a, expected]) + assert_that(bool(a)).is_equal(expected) + + +func test_parameterized_int_values(a: int, b: int, c: int, expected: int, _test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] ]) -> void: + + collect_test_call("test_parameterized_int_values", [a, b, c, expected]) + assert_that(a+b+c).is_equal(expected) + + +func test_parameterized_float_values(a: float, b: float, expected: float, _test_parameters := [ + [2.2, 2.2, 4.4], + [2.2, 2.3, 4.5], + [3.3, 2.2, 5.5] ]) -> void: + + collect_test_call("test_parameterized_float_values", [a, b, expected]) + assert_float(a+b).is_equal(expected) + + +func test_parameterized_string_values(a: String, b: String, expected: String, _test_parameters := [ + ["2.2", "2.2", "2.22.2"], + ["foo", "bar", "foobar"], + ["a", "b", "ab"] ]) -> void: + + collect_test_call("test_parameterized_string_values", [a, b, expected]) + assert_that(a+b).is_equal(expected) + + +func test_parameterized_Vector2_values(a: Vector2, b: Vector2, expected: Vector2, _test_parameters := [ + [Vector2.ONE, Vector2.ONE, Vector2(2, 2)], + [Vector2.LEFT, Vector2.RIGHT, Vector2.ZERO], + [Vector2.ZERO, Vector2.LEFT, Vector2.LEFT] ]) -> void: + + collect_test_call("test_parameterized_Vector2_values", [a, b, expected]) + assert_that(a+b).is_equal(expected) + + +func test_parameterized_Vector3_values(a: Vector3, b: Vector3, expected: Vector3, _test_parameters := [ + [Vector3.ONE, Vector3.ONE, Vector3(2, 2, 2)], + [Vector3.LEFT, Vector3.RIGHT, Vector3.ZERO], + [Vector3.ZERO, Vector3.LEFT, Vector3.LEFT] ]) -> void: + + collect_test_call("test_parameterized_Vector3_values", [a, b, expected]) + assert_that(a+b).is_equal(expected) + + +class TestObj extends RefCounted: + var _value: String + + func _init(value: String) -> void: + _value = value + + func _to_string() -> String: + return _value + + +func test_parameterized_obj_values(a: Object, b: Object, expected: String, _test_parameters := [ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"]]) -> void: + + collect_test_call("test_parameterized_obj_values", [a, b, expected]) + assert_that(a.to_string()+b.to_string()).is_equal(expected) + + +func test_parameterized_dict_values(data: Dictionary, expected: String, _test_parameters := [ + [{"key_a" : "value_a"}, '{"key_a":"value_a"}'], + [{"key_b" : "value_b"}, '{"key_b":"value_b"}'] + ]) -> void: + collect_test_call("test_parameterized_dict_values", [data, expected]) + assert_that(str(data).replace(" ", "")).is_equal(expected) + + +func test_dictionary_div_number_types( + value: Dictionary, + expected: Dictionary, + _test_parameters: Array = [ + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50, bottom = 50, left = 50, right = 50}], + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50, bottom = 50, left = 50, right = 50}], + ] +) -> void: + # allow to compare type unsave + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, false) + assert_that(value).is_equal(expected) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true) + + +func test_parameterized_untyped_array(items: Array, _test_parameters := [ + [[42]] + ] +) -> void: + collect_test_call("test_parameterized_untyped_array", [items]) + assert_array(items).contains_exactly([42]) + + +func test_parameterized_typed_array(items: Array[int], _test_parameters := [ + [[42]] + ] +) -> void: + collect_test_call("test_parameterized_typed_array", [items]) + assert_array(items).contains_exactly([42]) + + +func test_with_string_paramset( + values: Array, + expected: String, + _test_parameters : Array = [ + [ ["a"], "a" ], + [ ["a", "very", "long", "argument"], "a very long argument" ], + ] +) -> void: + var current := " ".join(values) + assert_that(current.strip_edges()).is_equal(expected) + + +# https://github.com/MikeSchulze/gdUnit4/issues/213 +func test_with_string_contains_brackets( + test_index: int, + value: String, + _test_parameters := [ + [1, "flowchart TD\nid>This is a flag shaped node]"], + [2, "flowchart TD\nid(((This is a double circle node)))"], + [3, "flowchart TD\nid((This is a circular node))"], + [4, "flowchart TD\nid>This is a flag shaped node]"], + [5, "flowchart TD\nid{'This is a rhombus node'}"], + [6, 'flowchart TD\nid((This is a circular node))'], + [7, 'flowchart TD\nid>This is a flag shaped node]'], + [8, 'flowchart TD\nid{"This is a rhombus node"}'], + [9, """ + flowchart TD + id{"This is a rhombus node"} + """], + ] +) -> void: + match test_index: + 1: assert_str(value).is_equal("flowchart TD\nid>This is a flag shaped node]") + 2: assert_str(value).is_equal("flowchart TD\nid(((This is a double circle node)))") + 3: assert_str(value).is_equal("flowchart TD\nid((This is a circular node))") + 4: assert_str(value).is_equal("flowchart TD\nid>" + "This is a flag shaped node]") + 5: assert_str(value).is_equal("flowchart TD\nid{'This is a rhombus node'}") + 6: assert_str(value).is_equal('flowchart TD\nid((This is a circular node))') + 7: assert_str(value).is_equal('flowchart TD\nid>This is a flag shaped node]') + 8: assert_str(value).is_equal('flowchart TD\nid{"This is a rhombus node"}') + 9: assert_str(value).is_equal(""" + flowchart TD + id{"This is a rhombus node"} + """) + + +func test_with_dynamic_parameter_resolving(name_: String, value: Variant, expected: Variant, _test_parameters := [ + ["test_a", auto_free(Node2D.new()), Node2D], + ["test_b", auto_free(Node3D.new()), Node3D], + ["test_c", _test_node_before, SubViewport], + ["test_d", _test_node_before_test, SubViewport], +]) -> void: + # all values must be resolved + assert_object(value).is_not_null().is_instanceof(expected) + if name_ == "test_c": + assert_object(value).is_same(_test_node_before) + if name_ == "test_d": + assert_object(value).is_same(_test_node_before_test) + # the argument '_test_parameters' must be replaced by set to avoid re-instantiate of test arguments + assert_array(_test_parameters).is_empty() + collect_test_call("test_with_dynamic_paramater_resolving", [name_]) + + +@warning_ignore("unused_parameter") +func test_with_dynamic_parameter_resolving2( + name_: String, + type: Variant, + log_level: Variant, + expected_logs: Dictionary, + _test_parameters := [ + ["test_a", null, "LOG", {}], + [ + "test_b", + Node2D, + null, + {Node2D: "ERROR"} + ], + [ + "test_c", + Node2D, + "LOG", + {Node2D: "LOG"} + ] + ] +) -> void: + # the argument '_test_parameters' must be replaced by set to avoid re-instantiate of test arguments + assert_array(_test_parameters).is_empty() + collect_test_call("test_with_dynamic_paramater_resolving2", [name_]) + + +var _test_set := [ + ["test_a"], + ["test_b"], + ["test_c"] +] + + +func test_with_extern_parameter_set(value: String, _test_parameters := _test_set) -> void: + assert_str(value).is_not_empty() + assert_array(_test_parameters).is_empty() + collect_test_call("test_with_extern_parameter_set", [value]) + + +const _data1 := ["aa"] +const _data2 := ["bb"] + + +func test_with_extern_const_parameter_set(value: String, _test_parameters := [_data1, _data2]) -> void: + assert_str(value).is_not_empty() + assert_array(_test_parameters).is_empty() + collect_test_call("test_with_extern_const_parameter_set", [value]) diff --git a/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd.uid b/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd.uid new file mode 100644 index 0000000..7d33b18 --- /dev/null +++ b/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd.uid @@ -0,0 +1 @@ +uid://f4cn02ox7mnr diff --git a/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd b/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd new file mode 100644 index 0000000..469093a --- /dev/null +++ b/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd @@ -0,0 +1,45 @@ +@warning_ignore_start("unsafe_method_access") +# GdUnit generated TestSuite +extends GdUnitTestSuite + + +var _handler :GdUnitCommandHandler + + +func before() -> void: + _handler = GdUnitCommandHandler.new() + + +func after() -> void: + _handler.free() + + +func test_command_shortcut() -> void: + assert_str(_handler.command_shortcut(GdUnitCommandInspectorRunTests.ID).get_as_text()).is_equal("Alt+F5") + assert_str(_handler.command_shortcut(GdUnitCommandInspectorDebugTests.ID).get_as_text()).is_equal("Alt+F6") + assert_str(_handler.command_shortcut(GdUnitCommandRunTestsOverall.ID).get_as_text()).is_equal("Alt+F7") + assert_str(_handler.command_shortcut(GdUnitCommandStopTestSession.ID).get_as_text()).is_equal("Alt+F8") + assert_str(_handler.command_shortcut(GdUnitCommandScriptEditorRunTests.ID).get_as_text()).is_equal("Ctrl+Alt+F5") + assert_str(_handler.command_shortcut(GdUnitCommandScriptEditorDebugTests.ID).get_as_text()).is_equal("Ctrl+Alt+F6") + assert_str(_handler.command_shortcut(GdUnitCommandScriptEditorCreateTest.ID).get_as_text()).is_equal("Ctrl+Alt+F10") + if Engine.get_version_info().hex >= 0x40600: + assert_str(_handler.command_shortcut(GdUnitCommandFileSystemRunTests.ID).get_as_text()).is_equal("Alt+Shift+F5") + assert_str(_handler.command_shortcut(GdUnitCommandFileSystemDebugTests.ID).get_as_text()).is_equal("Alt+Shift+F6") + else: + assert_str(_handler.command_shortcut(GdUnitCommandFileSystemRunTests.ID).get_as_text()).is_equal("Shift+Alt+F5") + assert_str(_handler.command_shortcut(GdUnitCommandFileSystemDebugTests.ID).get_as_text()).is_equal("Shift+Alt+F6") + + +## actually needs to comment out, it produces a lot of leaked instances +func _test__check_test_run_stopped_manually() -> void: + var inspector :GdUnitCommandHandler = mock(GdUnitCommandHandler, CALL_REAL_FUNC) + + # simulate no test is running + do_return(false).on(inspector).is_test_running_but_stop_pressed() + inspector.check_test_run_stopped_manually() + verify(inspector, 0).cmd_stop(any_int()) + + # simulate the test runner was manually stopped by the editor + do_return(true).on(inspector).is_test_running_but_stop_pressed() + inspector.check_test_run_stopped_manually() + verify(inspector, 1).cmd_stop() diff --git a/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd.uid b/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd.uid new file mode 100644 index 0000000..7561816 --- /dev/null +++ b/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd.uid @@ -0,0 +1 @@ +uid://cp1wrmxmrwrsq diff --git a/addons/gdUnit4/test/core/command/GdUnitShortcutTest.gd b/addons/gdUnit4/test/core/command/GdUnitShortcutTest.gd new file mode 100644 index 0000000..484a560 --- /dev/null +++ b/addons/gdUnit4/test/core/command/GdUnitShortcutTest.gd @@ -0,0 +1,15 @@ +# GdUnit generated TestSuite +class_name GdUnitShortcutTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source: String = 'res://addons/gdUnit4/src/core/command/GdUnitShortcut.gd' + + +func test_default_keys() -> void: + match OS.get_name().to_lower(): + 'windows': + assert_array(GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE)).is_empty() + assert_array(GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG)).contains_exactly(Key.KEY_ALT, Key.KEY_SHIFT, Key.KEY_F6) diff --git a/addons/gdUnit4/test/core/command/GdUnitShortcutTest.gd.uid b/addons/gdUnit4/test/core/command/GdUnitShortcutTest.gd.uid new file mode 100644 index 0000000..45be7e4 --- /dev/null +++ b/addons/gdUnit4/test/core/command/GdUnitShortcutTest.gd.uid @@ -0,0 +1 @@ +uid://dbms7lrtjwy10 diff --git a/addons/gdUnit4/test/core/discovery/GdUnitGuidTest.gd b/addons/gdUnit4/test/core/discovery/GdUnitGuidTest.gd new file mode 100644 index 0000000..3719370 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitGuidTest.gd @@ -0,0 +1,54 @@ +# GdUnit generated TestSuite +class_name GdUnitGuidTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/discovery/GdUnitGUID.gd' + + +func test_initialization() -> void: + # Test initialization with empty string + var guid_a := GdUnitGUID.new() + assert_str(guid_a._guid).is_not_empty() + + # Test initialization with existing GUID + var existing_guid := "12345678-abcd-efgh-ijkl-mnopqrstuvwx" + var guid_b := GdUnitGUID.new(existing_guid) + assert_str(guid_b._guid).is_equal(existing_guid) + + +func test_equals() -> void: + var guid_a := GdUnitGUID.new() + var guid_b := GdUnitGUID.new() + + assert_that(guid_a).is_equal(guid_a) + assert_that(guid_b).is_equal(guid_b) + assert_that(guid_a).is_not_equal(guid_b) + + assert_bool(guid_a.equals(guid_a)).is_true() + assert_bool(guid_a.equals(guid_b)).is_false() + assert_bool(guid_b.equals(guid_a)).is_false() + + +func test_performance() -> void: + var time := LocalTime.now() + var construction_count := 10000.0 + for n in construction_count: + GdUnitGUID.new() + var error_message := "Expected to construct %d `GdUnitGUID` instances in less than 50ms but took %s ms" % [construction_count, time.elapsed_since_ms()] + assert_int(time.elapsed_since_ms())\ + .override_failure_message(error_message)\ + .is_less(50) + prints("Construction of %d 'GdUnitGUID's tooks %d ms" % [construction_count, time.elapsed_since_ms()]) + + +func test_uniqueness() -> void: + var _guids := [] + for n in 5000: + var guid := GdUnitGUID.new() + assert_bool(_guids.has(guid))\ + .override_failure_message("Expected GUID to be unique but found a duplicate!")\ + .is_false() + _guids.append(guid) diff --git a/addons/gdUnit4/test/core/discovery/GdUnitGuidTest.gd.uid b/addons/gdUnit4/test/core/discovery/GdUnitGuidTest.gd.uid new file mode 100644 index 0000000..be692f1 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitGuidTest.gd.uid @@ -0,0 +1 @@ +uid://clgiq1x5dmna0 diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestCaseTest.gd b/addons/gdUnit4/test/core/discovery/GdUnitTestCaseTest.gd new file mode 100644 index 0000000..d79a970 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestCaseTest.gd @@ -0,0 +1,13 @@ +extends GdUnitTestSuite + + +func test_from() -> void: + var test := GdUnitTestCase.from( + "res://addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd", + "res://addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd", + 0, + "test_foo") + + assert_str(test.test_name).is_equal("test_foo") + assert_str(test.suite_name).is_equal("GdUnitInspectorTreeMainPanelTest") + assert_str(test.fully_qualified_name).is_equal("addons.gdUnit4.test.ui.parts.GdUnitInspectorTreeMainPanelTest.test_foo") diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestCaseTest.gd.uid b/addons/gdUnit4/test/core/discovery/GdUnitTestCaseTest.gd.uid new file mode 100644 index 0000000..2e1f1e9 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestCaseTest.gd.uid @@ -0,0 +1 @@ +uid://c22lmkfgbcjl diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverGuardTest.gd b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverGuardTest.gd new file mode 100644 index 0000000..8098383 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverGuardTest.gd @@ -0,0 +1,350 @@ +# GdUnit generated TestSuite +class_name GdUnitTestDiscoverGuardTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + + + +func test_inital() -> void: + var discoverer: GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new()) + + assert_dict(discoverer._discover_cache).is_empty() + + +func test_sync_cache() -> void: + # setup example tests + var test1 := GdUnitTestCase.from("res://test/my_test_suite.gd", "res://test/my_test_suite.gd", 23, "test_a") + var test2 := GdUnitTestCase.from("res://test/my_test_suite.gd", "res://test/my_test_suite.gd", 42, "test_b") + + # simulate running test dicovery + var discoverer: GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new()) + discoverer.sync_test_added(test1) + discoverer.sync_test_added(test2) + # verify the cache contains the discovered test id's + assert_dict(discoverer._discover_cache).contains_key_value("res://test/my_test_suite.gd", [test1, test2]) + + # simulate DISCOVER_START + discoverer.handle_discover_events(GdUnitEventTestDiscoverStart.new()) + # verify the cache is cleaned + assert_dict(discoverer._discover_cache).is_empty() + + +func test_discover_new_suite_GDScript() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + assert_that(script).is_not_null() + if script == null: + return + + var discoverer: GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new()) + # test initial the cache is empty + assert_dict(discoverer._discover_cache).is_empty() + + # simulate discovery of a new test suite + var discovered_tests: Array[GdUnitTestCase] = [] + # we overwrite the default discover sync to catch the tests and not emit `gdunit_test_discovered` + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + discovered_tests.append(test_case) + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + ) + + # verify the all tests are discovered + assert_array(discovered_tests).has_size(12) + assert_array(discovered_tests).extractv(extr("test_name"), extr("attribute_index")).contains_exactly([ + tuple("test_case1", -1), + tuple("test_case2", -1), + tuple("test_parameterized_static", 0), + tuple("test_parameterized_static", 1), + tuple("test_parameterized_static", 2), + tuple("test_parameterized_static_external", 0), + tuple("test_parameterized_static_external", 1), + tuple("test_parameterized_static_external", 2), + tuple("test_parameterized_dynamic", 0), + tuple("test_parameterized_dynamic", 1), + tuple("test_parameterized_dynamic", 2), + tuple("test_ๆ—ฅๆœฌ่ชž", -1), + ]) + + # verify the cache now contains all discovered tests + assert_dict(discoverer._discover_cache)\ + .contains_key_value(script.resource_path, discovered_tests) + + +func test_discover_deleted_test_GDScript() -> void: + var script := load_non_cached("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + # using debug mode to true to collect the change set + var discoverer: GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new(true)) + + var discovered_tests: Array[GdUnitTestCase] = [] + var expected_deleted_tests: Array[GdUnitTestCase] = [] + # simulate initial discovery of a new test suite + # we overwrite the default discover sync to catch the tests and not emit `gdunit_test_discovered` + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + discovered_tests.append(test_case) + # we save the tests for later verify to delete + if test_case.test_name in ["test_case1", "test_case2"]: + expected_deleted_tests.append(test_case) + ) + # verify the expected tests are collected + assert_array(expected_deleted_tests).has_size(2) + assert_array(discovered_tests).has_size(12) + + # we simmulate deleted tests + script.source_code = script.source_code.replace("test_case1", "_test_case1").replace("test_case2", "_test_case2") + assert_int(script.reload(true)).is_equal(OK) + + # calling the discover like when a script change is emited by a save action + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + ) + + # verify discovery detects the two deleted tests + var changed_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["changed_tests"] + var deleted_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["deleted_tests"] + var added_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["added_tests"] + assert_array(changed_tests).is_empty() + assert_array(deleted_tests).contains_exactly(expected_deleted_tests) + assert_array(added_tests).is_empty() + + # verify the cache now contains all discovered tests reduced by the deleted tests + assert_array(discoverer._discover_cache.get(script.resource_path))\ + .extractv(extr("guid"), extr("test_name"), extr("attribute_index"), extr("line_number"))\ + .contains_exactly([ + tuple(discovered_tests[2].guid, "test_parameterized_static", 0, 18), + tuple(discovered_tests[3].guid, "test_parameterized_static", 1, 18), + tuple(discovered_tests[4].guid, "test_parameterized_static", 2, 18), + tuple(discovered_tests[5].guid, "test_parameterized_static_external", 0, 26), + tuple(discovered_tests[6].guid, "test_parameterized_static_external", 1, 26), + tuple(discovered_tests[7].guid, "test_parameterized_static_external", 2, 26), + tuple(discovered_tests[8].guid, "test_parameterized_dynamic", 0, 32), + tuple(discovered_tests[9].guid, "test_parameterized_dynamic", 1, 32), + tuple(discovered_tests[10].guid, "test_parameterized_dynamic", 2, 32), + tuple(discovered_tests[11].guid, "test_ๆ—ฅๆœฌ่ชž", -1, 38), + ]) + + +func test_discover_added_test_GDScript() -> void: + var script := load_non_cached("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + # using debug mode to true to collect the change set + var discoverer: GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new(true)) + + var discovered_tests: Array[GdUnitTestCase] = [] + # simulate initial discovery of a new test suite + # we overwrite the default discover sync to catch the tests and not emit `gdunit_test_discovered` + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + discovered_tests.append(test_case) + ) + + # we simmulate adding two new tests + script.source_code += """ + func test_case3() -> void: + assert_bool(true).is_equal(true); + + + func test_case4() -> void: + assert_bool(false).is_equal(false); + """.dedent() + assert_int(script.reload(true)).is_equal(OK) + + var expected_added_tests: Array[GdUnitTestCase] = [] + # calling the discover like when a script change is emited by a save action + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + expected_added_tests.append(test_case) + ) + # verify the expected tests are collected + assert_array(expected_added_tests).has_size(2) + + # verify discovery detects the two deleted tests + var changed_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["changed_tests"] + var deleted_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["deleted_tests"] + var added_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["added_tests"] + assert_array(changed_tests).is_empty() + assert_array(deleted_tests).is_empty() + assert_array(added_tests).contains_exactly(expected_added_tests) + + # verify the cache now contains all discovered tests plus new discovered tests + assert_array(discoverer._discover_cache.get(script.resource_path))\ + .extractv(extr("guid"), extr("test_name"), extr("attribute_index"), extr("line_number"))\ + .contains_exactly([ + tuple(discovered_tests[0].guid, "test_case1", -1, 10), + tuple(discovered_tests[1].guid, "test_case2", -1, 14), + tuple(discovered_tests[2].guid, "test_parameterized_static", 0, 18), + tuple(discovered_tests[3].guid, "test_parameterized_static", 1, 18), + tuple(discovered_tests[4].guid, "test_parameterized_static", 2, 18), + tuple(discovered_tests[5].guid, "test_parameterized_static_external", 0, 26), + tuple(discovered_tests[6].guid, "test_parameterized_static_external", 1, 26), + tuple(discovered_tests[7].guid, "test_parameterized_static_external", 2, 26), + tuple(discovered_tests[8].guid, "test_parameterized_dynamic", 0, 32), + tuple(discovered_tests[9].guid, "test_parameterized_dynamic", 1, 32), + tuple(discovered_tests[10].guid, "test_parameterized_dynamic", 2, 32), + tuple(discovered_tests[11].guid, "test_ๆ—ฅๆœฌ่ชž", -1, 38), + tuple(expected_added_tests[0].guid, "test_case3", -1, 48), + tuple(expected_added_tests[1].guid, "test_case4", -1, 52), + ]) + + +func test_discover_renamed_test_GDScript() -> void: + var script := load_non_cached("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + # using debug mode to true to collect the change set + var discoverer: GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new(true)) + + var discovered_tests: Array[GdUnitTestCase] = [] + var expected_renamed_tests: Array[GdUnitTestCase] = [] + # simulate initial discovery of a new test suite + # we overwrite the default discover sync to catch the tests and not emit `gdunit_test_discovered` + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + discovered_tests.append(test_case) + # we save the tests for later verify as renemaed + if test_case.test_name in ["test_case1", "test_case2"]: + expected_renamed_tests.append(test_case) + ) + # verify the expected tests are collected + assert_array(expected_renamed_tests).has_size(2) + + # we simmulate deleted tests + script.source_code = script.source_code.replace("test_case1", "test_case11").replace("test_case2", "test_foo") + assert_int(script.reload(true)).is_equal(OK) + + # calling the discover like when a script change is emited by a save action + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + ) + + # verify discovery detects the two deleted tests + var changed_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["changed_tests"] + var deleted_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["deleted_tests"] + var added_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["added_tests"] + assert_array(changed_tests).contains_exactly(expected_renamed_tests) + assert_array(deleted_tests).is_empty() + assert_array(added_tests).is_empty() + + # verify the cache now contains all discovered tests inclusive renamed once + assert_array(discoverer._discover_cache.get(script.resource_path))\ + .extractv(extr("guid"), extr("test_name"), extr("attribute_index"), extr("line_number"))\ + .contains_exactly([ + tuple(discovered_tests[0].guid, "test_case11", -1, 10), + tuple(discovered_tests[1].guid, "test_foo", -1, 14), + tuple(discovered_tests[2].guid, "test_parameterized_static", 0, 18), + tuple(discovered_tests[3].guid, "test_parameterized_static", 1, 18), + tuple(discovered_tests[4].guid, "test_parameterized_static", 2, 18), + tuple(discovered_tests[5].guid, "test_parameterized_static_external", 0, 26), + tuple(discovered_tests[6].guid, "test_parameterized_static_external", 1, 26), + tuple(discovered_tests[7].guid, "test_parameterized_static_external", 2, 26), + tuple(discovered_tests[8].guid, "test_parameterized_dynamic", 0, 32), + tuple(discovered_tests[9].guid, "test_parameterized_dynamic", 1, 32), + tuple(discovered_tests[10].guid, "test_parameterized_dynamic", 2, 32), + tuple(discovered_tests[11].guid, "test_ๆ—ฅๆœฌ่ชž", -1, 38), + ]) + + +func test_discover_moved_test_GDScript() -> void: + var script := load_non_cached("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + # using debug mode to true to collect the change set + var discoverer: GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new(true)) + + var discovered_tests: Array[GdUnitTestCase] = [] + var expected_renamed_tests: Array[GdUnitTestCase] = [] + # simulate initial discovery of a new test suite + # we overwrite the default discover sync to catch the tests and not emit `gdunit_test_discovered` + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + discovered_tests.append(test_case) + # we save the tests for later verify as renemaed + if test_case.test_name in ["test_parameterized_static_external", "test_parameterized_dynamic"]: + expected_renamed_tests.append(test_case) + ) + # verify the expected tests are collected (2 test each with a dataset of 3 == 6) + assert_array(expected_renamed_tests).extractv(extr("test_name"), extr("attribute_index"), extr("line_number"))\ + .contains_exactly([ + tuple("test_parameterized_static_external", 0, 26), + tuple("test_parameterized_static_external", 1, 26), + tuple("test_parameterized_static_external", 2, 26), + tuple("test_parameterized_dynamic", 0, 32), + tuple("test_parameterized_dynamic", 1, 32), + tuple("test_parameterized_dynamic", 2, 32), + ]) + + # we insert two new lines before test test_parameterized_static_external (test source_line is now changed) + var source_code_index := script.source_code.find("func test_parameterized_static_external") + script.source_code = script.source_code.insert(source_code_index-1, "\n\n") + assert_int(script.reload(true)).is_equal(OK) + + # calling the discover like when a script change is emited by a save action + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + ) + + # verify discovery detects the moved tests by two lines + var changed_tests: Array[GdUnitTestCase] = discoverer._discovered_changes["changed_tests"] + assert_array(changed_tests).extractv(extr("guid"), extr("test_name"), extr("attribute_index"), extr("line_number"))\ + .contains_exactly([ + tuple(expected_renamed_tests[0].guid, "test_parameterized_static_external", 0, 28), + tuple(expected_renamed_tests[1].guid, "test_parameterized_static_external", 1, 28), + tuple(expected_renamed_tests[2].guid, "test_parameterized_static_external", 2, 28), + tuple(expected_renamed_tests[3].guid, "test_parameterized_dynamic", 0, 34), + tuple(expected_renamed_tests[4].guid, "test_parameterized_dynamic", 1, 34), + tuple(expected_renamed_tests[5].guid, "test_parameterized_dynamic", 2, 34), + tuple(discovered_tests[11].guid, "test_ๆ—ฅๆœฌ่ชž", -1, 40), + ]) + # and no added or removed tests + assert_array(discoverer._discovered_changes["deleted_tests"]).is_empty() + assert_array(discoverer._discovered_changes["added_tests"]).is_empty() + + # verify the cache contains all discovered tests inclusive line_number changes + assert_array(discoverer._discover_cache.get(script.resource_path))\ + .extractv(extr("guid"), extr("test_name"), extr("attribute_index"), extr("line_number"))\ + .contains_exactly([ + tuple(discovered_tests[0].guid, "test_case1", -1, 10), + tuple(discovered_tests[1].guid, "test_case2", -1, 14), + tuple(discovered_tests[2].guid, "test_parameterized_static", 0, 18), + tuple(discovered_tests[3].guid, "test_parameterized_static", 1, 18), + tuple(discovered_tests[4].guid, "test_parameterized_static", 2, 18), + # the following tests has line_number changes + tuple(discovered_tests[5].guid, "test_parameterized_static_external", 0, 28), + tuple(discovered_tests[6].guid, "test_parameterized_static_external", 1, 28), + tuple(discovered_tests[7].guid, "test_parameterized_static_external", 2, 28), + tuple(discovered_tests[8].guid, "test_parameterized_dynamic", 0, 34), + tuple(discovered_tests[9].guid, "test_parameterized_dynamic", 1, 34), + tuple(discovered_tests[10].guid, "test_parameterized_dynamic", 2, 34), + tuple(discovered_tests[11].guid, "test_ๆ—ฅๆœฌ่ชž", -1, 40), + ]) + + +#if GDUNIT4NET_API_V5 +func test_discover_on_csharp_script(_do_skip := !GdUnit4CSharpApiLoader.is_api_loaded()) -> void: + var discoverer :GdUnitTestDiscoverGuard = auto_free(GdUnitTestDiscoverGuard.new()) + var discovered_tests: Array[GdUnitTestCase] = [] + var script := load_non_cached("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.cs") + # simulate initial discovery of a new test suite + # we overwrite the default discover sync to catch the tests and not emit `gdunit_test_discovered` + await discoverer.discover(script, func(test_case: GdUnitTestCase) -> void: + # we need to manual update the cache here, this is normal made by gdunit_test_discovered signal + discoverer.sync_test_added(test_case) + discovered_tests.append(test_case) + ) + + assert_array(discovered_tests)\ + .extractv(extr("test_name"), extr("display_name"), extr("fully_qualified_name"))\ + .contains_exactly([ + tuple("TestCase1", "TestCase1", "gdUnit4.addons.gdUnit4.test.core.discovery.resources.DiscoverExampleTestSuite.TestCase1"), + tuple("TestCase2", "TestCase2", "gdUnit4.addons.gdUnit4.test.core.discovery.resources.DiscoverExampleTestSuite.TestCase2") + ]) +#endif + + +# we need to load the scripts freshly uncached because of script changes during test execution +func load_non_cached(resource_path: String) -> Script: + return ResourceLoader.load(resource_path, "Script", ResourceLoader.CACHE_MODE_IGNORE) diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverGuardTest.gd.uid b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverGuardTest.gd.uid new file mode 100644 index 0000000..df3f9d0 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverGuardTest.gd.uid @@ -0,0 +1 @@ +uid://btu7qkl3et8uj diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverSinkTest.gd b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverSinkTest.gd new file mode 100644 index 0000000..e6eba4b --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverSinkTest.gd @@ -0,0 +1,31 @@ +extends GdUnitTestSuite + +# example test discovery sink +class TestDiscoverSinkReceiver: + + var _discovered_tests: Array[GdUnitTestCase] + + func _init() -> void: + GdUnitSignals.instance().gdunit_test_discover_added.connect(on_test_case_discovered) + + func on_test_case_discovered(test_case: GdUnitTestCase) -> void: + _discovered_tests.append(test_case) + + +func test_discover() -> void: + # Create two example test cases + var test_a := GdUnitTestCase.new() + test_a.guid = GdUnitGUID.new() + test_a.test_name = "test_a" + var test_b := GdUnitTestCase.new() + test_b.guid = GdUnitGUID.new() + test_b.test_name = "test_a" + + # Create two discovery sinks + var receiver := TestDiscoverSinkReceiver.new() + GdUnitTestDiscoverSink.discover(test_a) + GdUnitTestDiscoverSink.discover(test_b) + GdUnitTestDiscoverSink.discover(test_b) + + # verify the sink contains all discovered tests + assert_array(receiver._discovered_tests).contains_exactly([test_a, test_b, test_b]) diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverSinkTest.gd.uid b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverSinkTest.gd.uid new file mode 100644 index 0000000..b467060 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverSinkTest.gd.uid @@ -0,0 +1 @@ +uid://mihs2gilqowk diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestDiscovererTest.gd b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscovererTest.gd new file mode 100644 index 0000000..8a15f99 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscovererTest.gd @@ -0,0 +1,153 @@ +extends GdUnitTestSuite + + +func test_discover_many_test() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + + var discovered_tests := [] + GdUnitTestDiscoverer.discover_tests(script, + func discover(test_case: GdUnitTestCase) -> void: + if test_case.test_name in ["test_case1", "test_case2", "test_parameterized_static"]: + discovered_tests.append(test_case) + ) + + assert_array(discovered_tests)\ + .extractv(extr("test_name"), extr("display_name"))\ + .contains_exactly([ + tuple("test_case1", "test_case1"), + tuple("test_case2", "test_case2"), + tuple("test_parameterized_static", "test_parameterized_static:0 (1, 1)"), + tuple("test_parameterized_static", "test_parameterized_static:1 (2, 2)"), + tuple("test_parameterized_static", "test_parameterized_static:2 (3, 3)"), + ]) + + +func test_discover_parameterized_test() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + + var discovered_tests := [] + GdUnitTestDiscoverer.discover_tests(script, + func discover(test_case: GdUnitTestCase) -> void: + if test_case.test_name == "test_parameterized_static": + discovered_tests.append(test_case) + ) + + assert_array(discovered_tests)\ + .extractv(extr("test_name"), extr("display_name"))\ + .contains_exactly([ + tuple("test_parameterized_static", "test_parameterized_static:0 (1, 1)"), + tuple("test_parameterized_static", "test_parameterized_static:1 (2, 2)"), + tuple("test_parameterized_static", "test_parameterized_static:2 (3, 3)"), + ]) + + +func test_discover_tests() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + + var discovered_tests := [] + GdUnitTestDiscoverer.discover_tests(script,\ + func discover(test_case: GdUnitTestCase) -> void: + discovered_tests.append(test_case) + ) + + assert_array(discovered_tests)\ + .extractv(extr("test_name"), extr("display_name"))\ + .contains_exactly([ + tuple("test_case1", "test_case1"), + tuple("test_case2", "test_case2"), + tuple("test_parameterized_static", "test_parameterized_static:0 (1, 1)"), + tuple("test_parameterized_static", "test_parameterized_static:1 (2, 2)"), + tuple("test_parameterized_static", "test_parameterized_static:2 (3, 3)"), + tuple("test_parameterized_static_external", "test_parameterized_static_external:0 ()"), + tuple("test_parameterized_static_external", "test_parameterized_static_external:1 (%s)" % Vector2.ONE), + tuple("test_parameterized_static_external", "test_parameterized_static_external:2 (%s)" % Vector2i.ONE), + tuple("test_parameterized_dynamic", "test_parameterized_dynamic:0 ()"), + tuple("test_parameterized_dynamic", "test_parameterized_dynamic:1 (%s)" % Vector2.ONE), + tuple("test_parameterized_dynamic", "test_parameterized_dynamic:2 (%s)" % Vector2i.ONE), + tuple("test_ๆ—ฅๆœฌ่ชž", "test_ๆ—ฅๆœฌ่ชž"), + ]) + + +func test_discover_tests_on_GdUnitTestSuite() -> void: + var script: GDScript = load("res://addons/gdUnit4/src/GdUnitTestSuite.gd") + + var discovered_tests := [] + GdUnitTestDiscoverer.discover_tests(script,\ + func discover(test_case: GdUnitTestCase) -> void: + discovered_tests.append(test_case) + ) + + # we expect no test covered from the base implementaion of GdUnitTestSuite + assert_array(discovered_tests).is_empty() + + +func test_discover_tests_inherited() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd") + + var discovered_tests := [] + GdUnitTestDiscoverer.discover_tests(script,\ + func discover(test_case: GdUnitTestCase) -> void: + discovered_tests.append(test_case) + ) + + assert_array(discovered_tests)\ + .extractv(extr("test_name"), extr("display_name"), extr("fully_qualified_name"))\ + .contains_exactly([ + tuple("test_foo3", "test_foo3", "addons.gdUnit4.test.core.resources.scan_testsuite_inheritance.by_class_name.ExtendsExtendedTest.test_foo3"), + tuple("test_foo2", "test_foo2", "addons.gdUnit4.test.core.resources.scan_testsuite_inheritance.by_class_name.ExtendsExtendedTest.test_foo2"), + tuple("test_foo1", "test_foo1", "addons.gdUnit4.test.core.resources.scan_testsuite_inheritance.by_class_name.ExtendsExtendedTest.test_foo1") + ]) + + +#if GDUNIT4NET_API_V5 +func test_discover_csharp_tests(_do_skip := !GdUnit4CSharpApiLoader.is_api_loaded()) -> void: + var script :Script = load("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.cs") + var discovered_tests := [] + GdUnitTestDiscoverer.discover_tests(script,\ + func discover(test_case: GdUnitTestCase) -> void: + discovered_tests.append(test_case) + ) + assert_array(discovered_tests)\ + .extractv(extr("test_name"), extr("display_name"), extr("fully_qualified_name"))\ + .contains_exactly([ + tuple("TestCase1", "TestCase1", "gdUnit4.addons.gdUnit4.test.core.discovery.resources.DiscoverExampleTestSuite.TestCase1"), + tuple("TestCase2", "TestCase2", "gdUnit4.addons.gdUnit4.test.core.discovery.resources.DiscoverExampleTestSuite.TestCase2") + ]) +#endif + + +func test_scan_test_directories() -> void: + assert_array(GdUnitTestDiscoverer.scan_test_directories("res://", "test", [])).contains_exactly([ + "res://addons/gdUnit4/test" + ]) + # for root folders + assert_array(GdUnitTestDiscoverer.scan_test_directories("res://", "", [])).contains_exactly([ + "res://addons", "res://assets" + ]) + assert_array(GdUnitTestDiscoverer.scan_test_directories("res://", "/", [])).contains_exactly([ + "res://addons", "res://assets" + ]) + assert_array(GdUnitTestDiscoverer.scan_test_directories("res://", "res://", [])).contains_exactly([ + "res://addons", "res://assets" + ]) + # a test folder not exists + assert_array(GdUnitTestDiscoverer.scan_test_directories("res://", "notest", [])).is_empty() + # for excluded folder (contain .gdignore file) + assert_array(GdUnitTestDiscoverer.scan_test_directories("res://", "excluded", [])).is_empty() + + +func test_scan_all_test_directories() -> void: + # Test when test_root_folder is empty + assert_array(GdUnitTestDiscoverer.scan_all_test_directories("")).contains_exactly(["res://"]) + + # Test when test_root_folder is "/" + assert_array(GdUnitTestDiscoverer.scan_all_test_directories("/")).contains_exactly(["res://"]) + + # Test when test_root_folder is "res://" + assert_array(GdUnitTestDiscoverer.scan_all_test_directories("res://")).contains_exactly(["res://"]) + + # Test when test_root_folder is set to a specific folder + assert_array(GdUnitTestDiscoverer.scan_all_test_directories("test")).contains_exactly(["res://addons/gdUnit4/test"]) + + # Test when test_root_folder is set to something which doesn't exist + assert_array(GdUnitTestDiscoverer.scan_all_test_directories("notest")).is_empty() diff --git a/addons/gdUnit4/test/core/discovery/GdUnitTestDiscovererTest.gd.uid b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscovererTest.gd.uid new file mode 100644 index 0000000..a216c68 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/GdUnitTestDiscovererTest.gd.uid @@ -0,0 +1 @@ +uid://t07kj8noqjef diff --git a/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.cs b/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.cs new file mode 100644 index 0000000..0e4d380 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.cs @@ -0,0 +1,18 @@ +namespace gdUnit4.addons.gdUnit4.test.core.discovery.resources; +#if GDUNIT4NET_API_V5 +using GdUnit4; + +using static GdUnit4.Assertions; + +[TestSuite] +public class DiscoverExampleTestSuite +{ + [TestCase] + public void TestCase1() + => AssertBool(true).IsEqual(true); + + [TestCase] + public void TestCase2() + => AssertBool(false).IsEqual(false); +} +#endif diff --git a/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd b/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd new file mode 100644 index 0000000..d028090 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd @@ -0,0 +1,46 @@ +extends GdUnitTestSuite + + +var _test_seta := [ + [null], + [Vector2.ONE], + [Vector2i.ONE], +] + +func test_case1() -> void: + assert_bool(true).is_equal(true); + + +func test_case2() -> void: + assert_bool(false).is_equal(false); + + +func test_parameterized_static(value: int, expected: int, _test_parameters := [ + [1, 1], + [2, 2], + [3, 3] +]) -> void: + assert_int(value).is_equal(expected); + + +func test_parameterized_static_external(value :Variant, _test_parameters := _test_seta) -> void: + assert_object(assert_vector(value))\ + .is_not_null()\ + .is_instanceof(GdUnitVectorAssert) + + +func test_parameterized_dynamic(value :Variant, _test_parameters := data_set()) -> void: + assert_object(assert_vector(value))\ + .is_not_null()\ + .is_instanceof(GdUnitVectorAssert) + + +func test_ๆ—ฅๆœฌ่ชž() -> void: + assert_bool(true).is_true() + + +func data_set() -> Array: + var test_values := [] + for index in 3: + test_values.append(_test_seta[index]) + return test_values diff --git a/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd.uid b/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd.uid new file mode 100644 index 0000000..3a4a826 --- /dev/null +++ b/addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd.uid @@ -0,0 +1 @@ +uid://b8copsh436pv5 diff --git a/addons/gdUnit4/test/core/event/GdUnitEventTest.gd b/addons/gdUnit4/test/core/event/GdUnitEventTest.gd new file mode 100644 index 0000000..cbe8aa2 --- /dev/null +++ b/addons/gdUnit4/test/core/event/GdUnitEventTest.gd @@ -0,0 +1,22 @@ +# GdUnit generated TestSuite +class_name GdUnitEventTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/event/GdUnitEvent.gd' + + +func test_GdUnitEvent_defaults() -> void: + var event := GdUnitEvent.new() + + assert_bool(event.is_success()).is_true() + assert_bool(event.is_warning()).is_false() + assert_bool(event.is_failed()).is_false() + assert_bool(event.is_error()).is_false() + assert_bool(event.is_skipped()).is_false() + + assert_int(event.elapsed_time()).is_zero() + assert_int(event.orphan_nodes()).is_zero() + assert_int(event.total_count()).is_zero() + assert_int(event.failed_count()).is_zero() + assert_int(event.skipped_count()).is_zero() diff --git a/addons/gdUnit4/test/core/event/GdUnitEventTest.gd.uid b/addons/gdUnit4/test/core/event/GdUnitEventTest.gd.uid new file mode 100644 index 0000000..9fd8721 --- /dev/null +++ b/addons/gdUnit4/test/core/event/GdUnitEventTest.gd.uid @@ -0,0 +1 @@ +uid://can57fc547oe6 diff --git a/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd b/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd new file mode 100644 index 0000000..e615adb --- /dev/null +++ b/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd @@ -0,0 +1,51 @@ +# this test test for serialization and deserialization succcess +# of GdUnitEvent class +extends GdUnitTestSuite + + +func test_serde_suite_before() -> void: + var event := GdUnitEvent.new().suite_before("path", "test_suite_a", 22) + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_object(deserialized).is_instanceof(GdUnitEvent) + assert_that(deserialized).is_equal(event) + + +func test_serde_suite_after() -> void: + var event := GdUnitEvent.new().suite_after("path","test_suite_a") + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + + +func test_serde_test_before() -> void: + var event := GdUnitEvent.new().test_before(GdUnitGUID.new()) + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + + +func test_serde_test_after_no_report() -> void: + var event := GdUnitEvent.new().test_after(GdUnitGUID.new(), "test_case_a") + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + + +func test_serde_test_after_with_report() -> void: + var reports :Array[GdUnitReport] = [\ + GdUnitReport.new().create(GdUnitReport.FAILURE, 24, "this is a error a"), \ + GdUnitReport.new().create(GdUnitReport.FAILURE, 26, "this is a error b")] + var event := GdUnitEvent.new().test_after(GdUnitGUID.new(), "test_case_a", {}, reports) + + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + assert_array(deserialized.reports()).contains_exactly(reports) + + +func test_serde_TestReport() -> void: + var report := GdUnitReport.new().create(GdUnitReport.FAILURE, 24, "this is a error") + var serialized := report.serialize() + var deserialized := GdUnitReport.new().deserialize(serialized) + assert_that(deserialized).is_equal(report) diff --git a/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd.uid b/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd.uid new file mode 100644 index 0000000..7776efe --- /dev/null +++ b/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd.uid @@ -0,0 +1 @@ +uid://dx70klqnbtc03 diff --git a/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd b/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd new file mode 100644 index 0000000..210ab31 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd @@ -0,0 +1,369 @@ +# GdUnit generated TestSuite +class_name GdUnitExecutionContextTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + + +func before() -> void: + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_CHECK, false) + + +func test_collect_report_statistics_with_errors() -> void: + # setup execution context tree like is build by the executor run + var ctx_suite := GdUnitExecutionContext.new("") + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(create_test_case("test_case1", 0, "")) + ts.add_child(tc) + ctx_suite.test_suite = ts + ctx_suite.orphan_monitor_start() + + # suite execution (GdUnitTestSuiteExecutor) + if ctx_suite != null: + var suite_err1 := ctx_suite.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 1, "suite before error")) + + # test execution (GdUnitTestSuiteExecutionStage) + var ctx_test := GdUnitExecutionContext.of_test_case(ctx_suite, tc) + if ctx_test != null: + # (GdUnitTestCaseSingleExecutionStage) + var ctx_test_hook := GdUnitExecutionContext.of(ctx_test) + var test_hook_err1 := ctx_test_hook.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 3, "before_test error")) + # test execution + var ctx_test_call := GdUnitExecutionContext.of(ctx_test_hook) + var test_err1 := ctx_test_call.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 13, "test error_a")) + var test_err2 := ctx_test_call.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 14, "test error_b")) + var test_err3 := ctx_test_call.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 15, "test error_c")) + ctx_test_call.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) + + var test_hook_err2 := ctx_test_hook.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 4, "after_test error")) + await get_tree().process_frame + ctx_test_hook.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) + + # verify + ctx_test.gc() + var test_reports := ctx_test.collect_reports(true) + var test_statisitcs := ctx_test.calculate_statistics(test_reports) + assert_array(test_reports).contains_exactly([test_hook_err1, test_hook_err2, test_err1, test_err2, test_err3]) + assert_dict(test_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: test_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: true, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 5, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + + var suite_err2 := ctx_suite.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 1, "suite after error")) + + # verify + ctx_suite.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + var suite_reports := ctx_suite.collect_reports(false) + assert_array(suite_reports).contains_exactly([suite_err1, suite_err2]) + var suite_statisitcs := ctx_suite.calculate_statistics(suite_reports) + assert_dict(suite_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: suite_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: true, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 2, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + ctx_suite.dispose() + + +func test_collect_report_statistics_with_errors_on_suite_hooks() -> void: + # setup execution context tree like is build by the executor run + var ctx_suite := GdUnitExecutionContext.new("") + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(create_test_case("test_case1", 0, "")) + ts.add_child(tc) + ctx_suite.test_suite = ts + ctx_suite.orphan_monitor_start() + + # suite execution (GdUnitTestSuiteExecutor) + if ctx_suite != null: + var suite_err1 := ctx_suite.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 1, "suite before error")) + + # test execution (GdUnitTestSuiteExecutionStage) + var ctx_test := GdUnitExecutionContext.of_test_case(ctx_suite, tc) + if ctx_test != null: + # (GdUnitTestCaseSingleExecutionStage) + var ctx_test_hook := GdUnitExecutionContext.of(ctx_test) + # test execution + var ctx_test_call := GdUnitExecutionContext.of(ctx_test_hook) + ctx_test_call.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) + + await get_tree().process_frame + ctx_test_hook.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) + + # verify + ctx_test.gc() + var test_reports := ctx_test.collect_reports(true) + var test_statisitcs := ctx_test.calculate_statistics(test_reports) + assert_array(test_reports).is_empty() + assert_dict(test_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: test_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + + var suite_err2 := ctx_suite.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 1, "suite after error")) + + # verify + ctx_suite.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + var suite_reports := ctx_suite.collect_reports(false) + assert_array(suite_reports).contains_exactly([suite_err1, suite_err2]) + var suite_statisitcs := ctx_suite.calculate_statistics(suite_reports) + assert_dict(suite_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: suite_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: true, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 2, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + ctx_suite.dispose() + + +func test_collect_report_statistics_only_errors_on_test_hooks() -> void: + # setup execution context tree like is build by the executor run + var ctx_suite := GdUnitExecutionContext.new("") + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(create_test_case("test_case1", 0, "")) + ts.add_child(tc) + ctx_suite.test_suite = ts + ctx_suite.orphan_monitor_start() + + # suite execution (GdUnitTestSuiteExecutor) + if ctx_suite != null: + # test execution (GdUnitTestSuiteExecutionStage) + var ctx_test := GdUnitExecutionContext.of_test_case(ctx_suite, tc) + if ctx_test != null: + # (GdUnitTestCaseSingleExecutionStage) + var ctx_test_hook := GdUnitExecutionContext.of(ctx_test) + + var err1 := ctx_test_hook.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 1, "error on before_test()")) + # test execution + var ctx_test_call := GdUnitExecutionContext.of(ctx_test_hook) + ctx_test_call.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) + + await get_tree().process_frame + ctx_test_hook.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) + + # verify + ctx_test.gc() + var test_reports := ctx_test.collect_reports(true) + var test_statisitcs := ctx_test.calculate_statistics(test_reports) + assert_array(test_reports).contains_exactly([err1]) + assert_dict(test_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: test_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: true, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 1, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + + # verify + ctx_suite.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + var suite_reports := ctx_suite.collect_reports(false) + assert_array(suite_reports).is_empty() + var suite_statisitcs := ctx_suite.calculate_statistics(suite_reports) + assert_dict(suite_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: suite_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + ctx_suite.dispose() + + +func test_collect_report_statistics_all_tests_skipped() -> void: + # setup execution context tree like is build by the executor run + var ctx_suite := GdUnitExecutionContext.new("") + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(create_test_case("test_case1", 0, "")) + ts.add_child(tc) + ctx_suite.test_suite = ts + ctx_suite.orphan_monitor_start() + + # suite execution (GdUnitTestSuiteExecutor) + if ctx_suite != null: + # test execution (GdUnitTestSuiteExecutionStage) + # simulate 10 test running as skipped + for index in range(0, 10): + var ctx_test := GdUnitExecutionContext.of_test_case(ctx_suite, tc) + if ctx_test != null: + var ctx_test_hook := GdUnitExecutionContext.of(ctx_test) + # test execution + var ctx_test_call := GdUnitExecutionContext.of(ctx_test_hook) + ctx_test_call.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) + var expected_report := ctx_test_call.add_report(GdUnitReport.new().create(GdUnitReport.SKIPPED, index*5, "skipped test %d" % index)) + await get_tree().process_frame + ctx_test_hook.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) + # verify + ctx_test.gc() + var test_reports := ctx_test.collect_reports(true) + var test_statisitcs := ctx_test.calculate_statistics(test_reports) + assert_array(test_reports).contains_exactly([expected_report]) + assert_dict(test_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: test_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 1, + GdUnitEvent.ORPHAN_NODES: 0, + }) + + # verify + ctx_suite.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + var suite_reports := ctx_suite.collect_reports(false) + assert_array(suite_reports).is_empty() + var suite_statisitcs := ctx_suite.calculate_statistics(suite_reports) + assert_dict(suite_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: suite_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + ctx_suite.dispose() + + +func test_simmulate_flaky_test(retry_count: int, is_flaky: bool, is_failed: bool, _test_parameters := [ + [1, false, true], + [2, false, true], + [3, true, false],]) -> void: + # setup execution context tree like is build by the executor run + var ctx_suite := GdUnitExecutionContext.new("") + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(create_test_case("test_case1", 0, "")) + ts.add_child(tc) + ctx_suite.test_suite = ts + ctx_suite.orphan_monitor_start() + + if ctx_suite != null: + # test execution + var ctx_test := GdUnitExecutionContext.of_test_case(ctx_suite, tc) + if ctx_test != null: + var expected_reports := [] + for retry in range(0, retry_count): + # before/after context + var ctx_test_hook := GdUnitExecutionContext.of(ctx_test) + # test context + var ctx_test_call := GdUnitExecutionContext.of(ctx_test_hook) + # let the first two retrys fail and the last retry succeeds + if retry < 2: + expected_reports.append(ctx_test_call.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "error"))) + expected_reports.append(ctx_test_call.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, 43, "error"))) + await get_tree().process_frame + ctx_test_hook.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) + + ctx_test.gc() + var reports := ctx_test.collect_reports(true) + assert_array(reports).contains_exactly(expected_reports) + var statistics := ctx_test.calculate_statistics(reports) + #for key: String in statistics.keys(): + # prints("%13s: %s" % [key, statistics[key]]) + #prints() + assert_dict(statistics).is_equal({ + GdUnitEvent.RETRY_COUNT: retry_count, + GdUnitEvent.ELAPSED_TIME: statistics[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: is_failed, + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: is_flaky, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 0 if is_flaky else retry_count*2, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + assert_bool(statistics[GdUnitEvent.FLAKY]).override_failure_message("Shold be marked as %s" % "flaky" if is_flaky else "").is_equal(is_flaky) + assert_bool(statistics[GdUnitEvent.FAILED]).override_failure_message("Expect be failing: %s" % is_failed).is_equal(is_failed) + + await get_tree().process_frame + ctx_suite.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + var suite_reports := ctx_suite.collect_reports(false) + assert_array(suite_reports).is_empty() + var suite_statisitcs := ctx_suite.calculate_statistics(suite_reports) + assert_dict(suite_statisitcs).is_equal({ + GdUnitEvent.RETRY_COUNT: 1, + GdUnitEvent.ELAPSED_TIME: suite_statisitcs[GdUnitEvent.ELAPSED_TIME], + GdUnitEvent.FAILED: false, # no suite hook failures + GdUnitEvent.ERRORS: false, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.FLAKY: false, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: 0, + GdUnitEvent.ORPHAN_NODES: 0, + }) + ctx_suite.dispose() + +static func create_test_case(p_name: String, p_line_number: int, p_script_path: String) -> _TestCase: + var test_case := GdUnitTestCase.new() + test_case.test_name = p_name + test_case.line_number = p_line_number + test_case.source_file = p_script_path + var attribute := TestCaseAttribute.new() + return _TestCase.new(test_case, attribute, null) + + +static func has_failures(context: GdUnitExecutionContext) -> bool: + var statistics := context.calculate_statistics(context.collect_reports(true)) + + return statistics[GdUnitEvent.FAILED] + + +static func count_failures(context: GdUnitExecutionContext) -> int: + var statistics := context.calculate_statistics(context.collect_reports(true)) + + return statistics[GdUnitEvent.FAILED_COUNT] diff --git a/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd.uid b/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd.uid new file mode 100644 index 0000000..fb52598 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd.uid @@ -0,0 +1 @@ +uid://cdrdkjal4rwcf diff --git a/addons/gdUnit4/test/core/execution/GdUnitProjectSettingsSnapshotTest.gd b/addons/gdUnit4/test/core/execution/GdUnitProjectSettingsSnapshotTest.gd new file mode 100644 index 0000000..e18a668 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitProjectSettingsSnapshotTest.gd @@ -0,0 +1,65 @@ +# GdUnit generated TestSuite +class_name GdUnitProjectSettingsSnapshotTest +extends GdUnitTestSuite + + +const __source = "res://addons/gdUnit4/src/core/execution/GdUnitProjectSettingsSnapshot.gd" + +const IGNORE := GdUnitSettings.GdScriptWarningMode.IGNORE +const WARN := GdUnitSettings.GdScriptWarningMode.WARN +const ERROR := GdUnitSettings.GdScriptWarningMode.ERROR +const EXCLUDE := GdUnitSettings.GdScriptWarningDirectoryMode.EXCLUDE +const INCLUDE := GdUnitSettings.GdScriptWarningDirectoryMode.INCLUDE + + +#region save / restore + +func test_restore_restores_scalar_setting() -> void: + # Setup + var snapshot := GdUnitProjectSettingsSnapshot.new() + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, IGNORE) + snapshot.save() + # Act + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, ERROR) + snapshot.restore() + # Verify + assert_int(ProjectSettings.get_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION))\ + .is_equal(IGNORE) + + +func test_restore_restores_dictionary_setting() -> void: + # Setup + var snapshot := GdUnitProjectSettingsSnapshot.new() + var original_rules: Dictionary = {"res://addons": EXCLUDE} + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, original_rules.duplicate()) + snapshot.save() + # Act + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES, {"res://modified": INCLUDE}) + snapshot.restore() + # Verify + assert_dict(ProjectSettings.get_setting(GdUnitSettings.GDSCRIPT_WARNINGS_DIRECTORY_RULES))\ + .is_equal(original_rules) + + +func test_restore_does_not_change_unmodified_settings() -> void: + # Setup + var snapshot := GdUnitProjectSettingsSnapshot.new() + ProjectSettings.set_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION, IGNORE) + snapshot.save() + # Act + snapshot.restore() + # Verify + assert_int(ProjectSettings.get_setting(GdUnitSettings.GDSCRIPT_WARNINGS_INFERRED_DECLARATION))\ + .is_equal(IGNORE) + + +func test_restore_without_save_does_nothing() -> void: + # Setup + var snapshot := GdUnitProjectSettingsSnapshot.new() + # Act + snapshot.restore() + snapshot.restore() + # Verify โ€” no error, no crash + assert_bool(true).is_true() + +#endregion diff --git a/addons/gdUnit4/test/core/execution/GdUnitProjectSettingsSnapshotTest.gd.uid b/addons/gdUnit4/test/core/execution/GdUnitProjectSettingsSnapshotTest.gd.uid new file mode 100644 index 0000000..9c5128a --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitProjectSettingsSnapshotTest.gd.uid @@ -0,0 +1 @@ +uid://gc8bepn2nvda diff --git a/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd b/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd new file mode 100644 index 0000000..baa14db --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd @@ -0,0 +1,979 @@ +# GdUnit generated TestSuite +class_name GdUnitTestSuiteExecutorTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd' +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +const SUCCEEDED = true +const FAILED = false +const SKIPPED = true +const NOT_SKIPPED = false +const FLAKY = true +const FAILURES = true +const NO_FAILURES = false +const ERRORS = true +const NO_ERRORS = false +const WARNINGS = true +const NO_WARNING = false + +var _collected_events: Array[GdUnitEvent] = [] +func before() -> void: + GdUnitSignals.instance().gdunit_event_debug.connect(_on_gdunit_event_debug) + # we run without flaky check + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_CHECK, false) + + +func after() -> void: + GdUnitSignals.instance().gdunit_event_debug.disconnect(_on_gdunit_event_debug) + + +func after_test() -> void: + _collected_events.clear() + + +func _on_gdunit_event_debug(event :GdUnitEvent) -> void: + _collected_events.append(event) + + +func flating_message(message :String) -> String: + return GdUnitTools.richtext_normalize(message) + + +func run_tests(tests :Array[GdUnitTestCase], settings := {}) -> Array[GdUnitEvent]: + # run in a separate context to not affect the current test run + await GdUnitThreadManager.run("test_executor_%d" % randi(), func() -> void: + var executor := GdUnitTestSuiteExecutor.new(true) + + # apply custom run settints + var saves_settings := {} + for key: String in settings.keys(): + saves_settings[key] = ProjectSettings.get_setting(key) + ProjectSettings.set_setting(key, settings[key]) + + # sync to main thread + await get_tree().process_frame + # execute all tests + await executor.run_and_wait(tests) + + # restore settings + for key: String in saves_settings.keys(): + ProjectSettings.set_setting(key, saves_settings[key]) + + ) + return _collected_events + + +func assert_event_list(events :Array[GdUnitEvent], suite_name :String, test_case_names :Array[String]) -> void: + var expected_events := Array() + expected_events.append(tuple(GdUnitEvent.TESTSUITE_BEFORE, suite_name, any_class(GdUnitGUID), test_case_names.size())) + for test_case in test_case_names: + expected_events.append(tuple(GdUnitEvent.TESTCASE_BEFORE, suite_name, test_case, 0)) + expected_events.append(tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, test_case, 0)) + expected_events.append(tuple(GdUnitEvent.TESTSUITE_AFTER, suite_name, any_class(GdUnitGUID), 0)) + + # the suite hooks 2 + (test hocks 2 * test count) + var expected_event_count := 2 + test_case_names.size() * 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + assert_array(events)\ + .extractv(extr("type"), extr("suite_name"), extr("test_name"), extr("total_count"))\ + .contains_exactly(expected_events) + + +func assert_test_counters(events :Array[GdUnitEvent]) -> GdUnitArrayAssert: + var _events := events.filter(func(event: GdUnitEvent) -> bool: + return event.type() in [GdUnitEvent.TESTSUITE_BEFORE, GdUnitEvent.TESTSUITE_AFTER, GdUnitEvent.TESTCASE_BEFORE, GdUnitEvent.TESTCASE_AFTER] + ) + return assert_array(_events).extractv(extr("guid"), extr("type"), extr("error_count"), extr("failed_count"), extr("orphan_nodes")) + + +func assert_event_states(events :Array[GdUnitEvent]) -> GdUnitArrayAssert: + var _events := events.filter(func(event: GdUnitEvent) -> bool: + return event.type() in [GdUnitEvent.TESTSUITE_BEFORE, GdUnitEvent.TESTSUITE_AFTER, GdUnitEvent.TESTCASE_BEFORE, GdUnitEvent.TESTCASE_AFTER] + ) + return assert_array(_events).extractv(extr("guid"), extr("is_success"), extr("is_skipped"), extr("is_warning"), extr("is_failed"), extr("is_error")) + + +@warning_ignore("unsafe_method_access", "unsafe_cast") +func assert_event_reports(events: Array[GdUnitEvent], expected_reports: Array) -> void: + var _events: Array[GdUnitEvent] = events + for event_index in _events.size(): + var event := _events[event_index] + var current: Array[GdUnitReport] = event.reports() + var expected: Array = expected_reports[event_index] if expected_reports.size() > event_index else [] + if expected.is_empty(): + for m in current.size(): + assert_str(flating_message(current[m].message() as String)).is_empty() + for m in expected.size(): + if m < current.size(): + assert_str(flating_message(current[m].message() as String)).append_failure_message(event.test_name()).starts_with(str(expected[m])) + else: + assert_str("").is_equal(expected[m]) + + +func test_execute_success() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # verify all counters are zero / no errors, failures, orphans + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # all tests passses + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # suite succeeds + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + # all success no reports expected + assert_event_reports(events, [ + [], [], [], [], [], [] + ]) + + +func test_execute_failure_on_stage_before() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect the testsuite is failing on stage 'before()' and commits one failure + # reported finally at TESTSUITE_AFTER event + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + # report failure failed_count = 1 + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 1, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # report suite is not success, is failed + tuple(any_class(GdUnitGUID), FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + ]) + # one failure at before() + assert_event_reports(events, [ + [], + [], + [], + [], + [], + ["failed on before()"] + ]) + + +func test_execute_failure_on_stage_after() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect the testsuite is failing on stage 'after()' and commits one failure + # reported finally at TESTSUITE_AFTER event + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + # report failure failed_count = 1 + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 1, 0), + ]) + # all tests passses + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # report suite faile on hook after() + tuple(any_class(GdUnitGUID), FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + ]) + # one failure at after() + assert_event_reports(events, [ + [], + [], + [], + [], + [], + ["failed on after()"] + ]) + + +func test_execute_failure_on_stage_before_test() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect the testsuite is failing on stage 'before_test()' and commits one failure on each test case + # because is in scope of test execution + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # failure is count to the test + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + # failure is count to the test + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # each test must be failed because of the failure on before_test() hook + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + # report suite succeeds, no failures on suite hooks + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + # before_test() failure report is append to each test + assert_event_reports(events, [ + [], + [], + # verify failure report is append to 'test_case1' + ["failed on before_test()"], + [], + # verify failure report is append to 'test_case2' + ["failed on before_test()"], + [] + ]) + + +func test_execute_failure_on_stage_after_test() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect the testsuite is failing on stage 'after_test()' and commits one failure on each test case + # because is in scope of test execution + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # failure is count to the test + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # failure is count to the test + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # each test must be failed because of the failure on after_test() hook + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + # report suite succeeds, no failures on suite hooks + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + + # 'after_test' failure report is append to each test + assert_event_reports(events, [ + [], + [], + # verify failure report is append to 'test_case1' + ["failed on after_test()"], + [], + # verify failure report is append to 'test_case2' + ["failed on after_test()"], + [] + ]) + + +func test_execute_failure_on_stage_test_case1() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect the test case 'test_case1' is failing and commits one failure + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # test has one failure + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # test_case1 is failing only + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # report suite succeeds, no failures on the suite hooks + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + # only 'test_case1' reports a failure + assert_event_reports(events, [ + [], + [], + # verify failure report is append to 'test_case1' + ["failed on test_case1()"], + [], + [], + [] + ]) + + +func test_execute_failure_on_multiple_stages() -> void: + # this is a more complex failure state, we expect to find multipe failures on different stages + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect failing on multiple stages + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the first test has two failures plus one from 'before_test' + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 3, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the second test has no failures but one from 'before_test' + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + # and one failure is on stage 'after' found + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 1, 0), + ]) + # each test is failing in addition to failing test hooks + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + # report suite is not success, is failed on after() hook + tuple(any_class(GdUnitGUID), FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + ]) + # only 'test_case1' reports a 'real' failures plus test setup stage failures + assert_event_reports(events, [ + [], + [], + # verify failure reports to 'test_case1' + ["failed on before_test()", "failed 1 on test_case1()", "failed 2 on test_case1()"], + [], + # verify failure reports to 'test_case2' + ["failed on before_test()"], + # and one failure detected at stage 'after' + ["failed on after()"] + ]) + + +# GD-63 +func test_execute_failure_and_orphans() -> void: + # this is a more complex failure state, we expect to find multipe orphans on different stages + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteFailAndOrpahnsDetected.gd") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect failing on multiple stages + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the first test ends with a warning and in summ 5 orphans detected + # 2 from stage 'before_test' + 3 from test itself + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 5), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the second test ends with a one failure and in summ 6 orphans detected + # 2 from stage 'before_test' + 4 from test itself + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 6), + # and one orphan detected from stage 'before' + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 1), + ]) + # is_success, is_warning, is_failed, is_error + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # test case succeeds with warnings + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, WARNINGS, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # test case has failures and warnings + tuple(test_case2.guid, FAILED, NOT_SKIPPED, WARNINGS, FAILURES, NO_ERRORS), + # report suite is success but orphan warnings and failures + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, WARNINGS, NO_FAILURES, NO_ERRORS), + ]) + # only 'test_case1' reports a 'real' failures plus test setup stage failures + assert_event_reports(events, [ + [], + [], + # ends with warnings + ["WARNING: Detected 2 possible orphan nodes.", + "WARNING: Detected 3 orphan nodes."], + [], + # ends with failure and warnings + ["WARNING: Detected 2 possible orphan nodes.", + "faild on test_case2()"], + # and one warning detected at stage 'after' + ["WARNING: Detected 1 possible orphan nodes."] + ]) + + +func test_execute_failure_and_orphans_report_orphan_disabled() -> void: + # this is a more complex failure state, we expect to find multipe orphans on different stages + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteFailAndOrpahnsDetected.gd") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + + # simulate test suite execution whit disabled orphan detection + var events := await run_tests(all_tests, { + GdUnitSettings.REPORT_ORPHANS: false + }) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect failing on multiple stages, no orphans reported + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # one failure + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # is_success, is_warning, is_failed, is_error + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # test case has success + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # test case has a failure + tuple(test_case2.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + # report suite succeeds with warnings and failures + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + # only 'test_case1' reports a failure, orphans are not reported + assert_event_reports(events, [ + [], + [], + [], + [], + # ends with a failure + ["faild on test_case2()"], + [] + ]) + + +func test_execute_error_on_test_timeout() -> void: + # this tests a timeout on a test case reported as error + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect test_case1 fails by a timeout + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + # the first test timed out after 2s + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 1, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # test_case1 is interrupted by a timeout and fails + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # testcase ends with a timeout error + tuple(test_case1.guid, FAILED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + # report suite has no failures and errors + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + # 'test_case1' reports a error triggered by test timeout + assert_event_reports(events, [ + [], + [], + # verify error reports to 'test_case1' + ["Timeout !\n 'Test timed out after 2s 0ms'"], + [], + [], + [] + ]) + + +# This test checks if all test stages are called at each test iteration. +func test_execute_fuzzed_metrics() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.gd") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + # simulate test suite execution + var events := await run_tests(all_tests) + assert_event_states(events).contains([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + assert_event_reports(events, [ + [], + [], + [], + [], + [], + [] + ]) + + +# This test checks if all test stages are called at each test iteration. +func test_execute_parameterized_metrics() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.gd") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + # simulate test suite execution + var events := await run_tests(all_tests) + assert_event_states(events).contains([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + assert_event_reports(events, [ + [], + [], + [], + [], + [], + [] + ]) + + +func test_execute_failure_fuzzer_iteration() -> void: + # this tests a timeout on a test case reported as error + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_multi_yielding_with_fuzzer: GdUnitTestCase = tests["test_multi_yielding_with_fuzzer"] + var test_multi_yielding_with_fuzzer_fail_after_3_iterations: GdUnitTestCase = tests["test_multi_yielding_with_fuzzer_fail_after_3_iterations"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # we expect failing at 'test_multi_yielding_with_fuzzer_fail_after_3_iterations' after three iterations + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_multi_yielding_with_fuzzer.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_multi_yielding_with_fuzzer.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(test_multi_yielding_with_fuzzer_fail_after_3_iterations.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # test failed after 3 iterations + tuple(test_multi_yielding_with_fuzzer_fail_after_3_iterations.guid, GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # is_success, is_warning, is_failed, is_error + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_multi_yielding_with_fuzzer.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_multi_yielding_with_fuzzer.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_multi_yielding_with_fuzzer_fail_after_3_iterations.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_multi_yielding_with_fuzzer_fail_after_3_iterations.guid, FAILED, NOT_SKIPPED, NO_WARNING, FAILURES, NO_ERRORS), + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS) + ]) + # 'test_case1' reports a error triggered by test timeout + assert_event_reports(events, [ + [], + [], + [], + [], + # must fail after three iterations + ["Found an error after '3' test iterations\n Expecting: 'false' but is 'true'"], + [] + ]) + + +func test_execute_add_child_on_before_GD_106() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # verify all counters are zero / no errors, failures, orphans + assert_test_counters(events).contains_exactly([ + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case1.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(test_case2.guid, GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(any_class(GdUnitGUID), GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + # all success no reports expected + assert_event_reports(events, [ + [], [], [], [], [], [] + ]) + + +func test_execute_parameterizied_tests() -> void: + # this is a more complex failure state, we expect to find multipe failures on different stages + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_0: GdUnitTestCase = find_test(tests, "test_dictionary_div_number_types:0") + var test_1: GdUnitTestCase = find_test(tests, "test_dictionary_div_number_types:1") + var test_2: GdUnitTestCase = find_test(tests, "test_dictionary_div_number_types:2") + var test_3: GdUnitTestCase = find_test(tests, "test_dictionary_div_number_types:3") + + # simulate test suite execution + # run the tests with to compare type save + var original_mode :Variant = ProjectSettings.get_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true) + var events := await run_tests(all_tests, { + GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE : true + }) + # the test is partial failing because of diverent type in the dictionary + assert_array(events).extractv( + extr("type"), extr("guid"), extr("is_warning"), extr("is_error"), extr("is_failed"), extr("orphan_nodes"))\ + .contains([ + tuple(GdUnitEvent.TESTCASE_AFTER, test_0.guid, NO_WARNING, NO_ERRORS, FAILURES, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, test_1.guid, NO_WARNING, NO_ERRORS, NO_FAILURES, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, test_2.guid, NO_WARNING, NO_ERRORS, FAILURES, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, test_3.guid, NO_WARNING, NO_ERRORS, NO_FAILURES, 0) + ]) + + # rerun the same tests again with allow to compare type unsave + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, false) + # simulate test suite execution + events = await run_tests(all_tests) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, original_mode) + + # the test should now be successful + assert_array(events).extractv( + extr("type"), extr("guid"), extr("is_warning"), extr("is_error"), extr("is_failed"), extr("orphan_nodes"))\ + .contains([ + tuple(GdUnitEvent.TESTCASE_AFTER, test_0.guid, NO_WARNING, NO_ERRORS, NO_FAILURES, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, test_1.guid, NO_WARNING, NO_ERRORS, NO_FAILURES, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, test_2.guid, NO_WARNING, NO_ERRORS, NO_FAILURES, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, test_3.guid, NO_WARNING, NO_ERRORS, NO_FAILURES, 0) + ]) + + +func test_execute_test_suite_is_skipped() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # the entire test-suite is skipped + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(any_class(GdUnitGUID), SUCCEEDED, SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + assert_event_reports(events, [ + [], + [], + [""" + This test is skipped! + Reason: 'Skipped from the entire test suite' + """.dedent().trim_prefix("\n")], + [], + [""" + This test is skipped! + Reason: 'Skipped from the entire test suite' + """.dedent().trim_prefix("\n")], + # must fail after three iterations + [""" + The Entire test-suite is skipped! + Skipped '2' tests + Reason: 'do not run this' + """.dedent().trim_prefix("\n")] + ]) + + +func test_execute_test_case_is_skipped() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_case1: GdUnitTestCase = tests["test_case1"] + var test_case2: GdUnitTestCase = tests["test_case2"] + # simulate test suite execution + var events := await run_tests(all_tests) + + # (before_test+after_test) * test count + before+after hooks + var expected_event_count := tests.size() * 2 + 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + # the test_case1 is skipped + assert_event_states(events).contains_exactly([ + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case1.guid, SUCCEEDED, SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(test_case2.guid, SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + tuple(any_class(GdUnitGUID), SUCCEEDED, NOT_SKIPPED, NO_WARNING, NO_FAILURES, NO_ERRORS), + ]) + + assert_event_reports(events, [ + [], + [], + [""" + This test is skipped! + Reason: 'do not run this' + """.dedent().trim_prefix("\n")], + [], + [], + [] + ]) + + +func test_execute_test_case_is_flaky_and_failed() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestCaseFlaky.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_success := find_test(tests, "test_success") + var test_flaky_success := find_test(tests, "test_flaky_success") + var test_flaky_fail := find_test(tests, "test_flaky_fail") + var test_parameterized_flaky0 := find_test(tests, "test_parameterized_flaky:0") + var test_parameterized_flaky1 := find_test(tests, "test_parameterized_flaky:1") + var test_parameterized_flaky2 := find_test(tests, "test_parameterized_flaky:2") + var test_parameterized_flaky3 := find_test(tests, "test_parameterized_flaky:3") + var test_parameterized_flaky4 := find_test(tests, "test_parameterized_flaky:4") + var test_fuzzed_flaky_success := find_test(tests, "test_fuzzed_flaky_success") + var test_fuzzed_flaky_fail := find_test(tests, "test_fuzzed_flaky_fail") + + # simulate flaky test suite execution + var events := await run_tests(all_tests, { + GdUnitSettings.TEST_FLAKY_CHECK : true, + GdUnitSettings.TEST_FLAKY_MAX_RETRIES : 5 + }) + + assert_array(events).extractv(extr("type"), extr("guid"), extr("is_flaky"), extr("is_success"))\ + .contains([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, any_class(GdUnitGUID), false, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_success.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_flaky_success.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_flaky_fail.guid, !FLAKY, FAILED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky0.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky1.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky2.guid, !FLAKY, FAILED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky3.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky4.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_fuzzed_flaky_success.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_fuzzed_flaky_fail.guid, !FLAKY, FAILED), + tuple(GdUnitEvent.TESTSUITE_AFTER, any_class(GdUnitGUID), !FLAKY, SUCCEEDED), + ]) + + +func test_execute_test_case_is_flaky_and_success() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/resources/testsuites/TestCaseFlaky.resource") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var test_success := find_test(tests, "test_success") + var test_flaky_success := find_test(tests, "test_flaky_success") + var test_flaky_fail := find_test(tests, "test_flaky_fail") + var test_parameterized_flaky0 := find_test(tests, "test_parameterized_flaky:0") + var test_parameterized_flaky1 := find_test(tests, "test_parameterized_flaky:1") + var test_parameterized_flaky2 := find_test(tests, "test_parameterized_flaky:2") + var test_parameterized_flaky3 := find_test(tests, "test_parameterized_flaky:3") + var test_parameterized_flaky4 := find_test(tests, "test_parameterized_flaky:4") + var test_fuzzed_flaky_success := find_test(tests, "test_fuzzed_flaky_success") + var test_fuzzed_flaky_fail := find_test(tests, "test_fuzzed_flaky_fail") + + # simulate flaky test suite execution + var events := await run_tests(all_tests, { + GdUnitSettings.TEST_FLAKY_CHECK : true, + GdUnitSettings.TEST_FLAKY_MAX_RETRIES : 6 + }) + + # verify test execution results. all test should pass at least after 6 retries + assert_array(events).extractv(extr("type"), extr("guid"), extr("is_flaky"), extr("is_success"))\ + .contains([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, any_class(GdUnitGUID), false, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_success.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_flaky_success.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_flaky_fail.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky0.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky1.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky2.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky3.guid, !FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_parameterized_flaky4.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_fuzzed_flaky_success.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, test_fuzzed_flaky_fail.guid, FLAKY, SUCCEEDED), + tuple(GdUnitEvent.TESTSUITE_AFTER, any_class(GdUnitGUID), !FLAKY, SUCCEEDED), + ]) + + +func test_execute_push_error_monitoring_disabled() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/execution/resources/error_monitoring/TestSuiteWithPushErrorTests.gd") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + # execute all tests + var events := await run_tests(all_tests, { + GdUnitSettings.REPORT_PUSH_ERRORS : false + }) + + assert_array(events).extractv(extr("type"), extr("test_name"), extr("is_success"))\ + .contains([ + tuple(GdUnitEvent.TESTCASE_AFTER, "test_without_push_errors", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_with_push_error", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_with_push_error_is_catched", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_without_push_warning", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_with_push_warning", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_push_warning_is_catched", SUCCEEDED), + tuple(GdUnitEvent.TESTSUITE_AFTER, "after", SUCCEEDED), + ]) + + +func test_execute_push_error_monitoring_enabled() -> void: + var tests := GdUnitTestResourceLoader.load_tests("res://addons/gdUnit4/test/core/execution/resources/error_monitoring/TestSuiteWithPushErrorTests.gd") + var all_tests: Array[GdUnitTestCase] = Array(tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + # execute all tests + var events := await run_tests(all_tests, { + GdUnitSettings.REPORT_PUSH_ERRORS : true + }) + + assert_array(events).extractv(extr("type"), extr("test_name"), extr("is_success"))\ + .contains([ + tuple(GdUnitEvent.TESTCASE_AFTER, "test_without_push_errors", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_with_push_error", FAILED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_with_push_error_is_catched", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_without_push_warning", SUCCEEDED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_with_push_warning", FAILED), + tuple(GdUnitEvent.TESTCASE_AFTER, "test_push_warning_is_catched", SUCCEEDED), + tuple(GdUnitEvent.TESTSUITE_AFTER, "after", SUCCEEDED), + ]) + + +func filter_by_test_case(events: Array[GdUnitEvent], test: GdUnitTestCase) -> Array[GdUnitEvent]: + return events.filter(func (event: GdUnitEvent) -> bool: + return event.guid().equals(test.guid) and event.type() == GdUnitEvent.TESTCASE_AFTER + ) + + +func find_test(tests: Dictionary, test_name: String) -> GdUnitTestCase: + for key: String in tests.keys(): + if key.begins_with(test_name): + return tests[key] + return null diff --git a/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd.uid b/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd.uid new file mode 100644 index 0000000..41283e1 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd.uid @@ -0,0 +1 @@ +uid://c8xoxvyqrxt1v diff --git a/addons/gdUnit4/test/core/execution/resources/OrphanScene.tscn b/addons/gdUnit4/test/core/execution/resources/OrphanScene.tscn new file mode 100644 index 0000000..8e0a513 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/OrphanScene.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=2 format=3 uid="uid://c13on7kf6135b"] + +[sub_resource type="GDScript" id="GDScript_qdgw4"] +script/source = "extends Node2D + +var x1 := Node2D.new() +var t2 := T2.new() + +func _ready() -> void: + var _x2 := Node2D.new() + add_child(t2) + add_child(_x2) +" + +[node name="OrphanScene" type="Node2D"] +script = SubResource("GDScript_qdgw4") diff --git a/addons/gdUnit4/test/core/execution/resources/error_monitoring/.gdignore b/addons/gdUnit4/test/core/execution/resources/error_monitoring/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/core/execution/resources/error_monitoring/ExampleClassWithPushNotifications.gd b/addons/gdUnit4/test/core/execution/resources/error_monitoring/ExampleClassWithPushNotifications.gd new file mode 100644 index 0000000..0f8894c --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/error_monitoring/ExampleClassWithPushNotifications.gd @@ -0,0 +1,19 @@ +class_name ExampleClassWithPushNotifications +extends Node + +const ERROR_NEGATIVE_VALUE := "Cannot get square root of negative value" +const WARN_UNCHANGED := "Text was not changed" + + +static func get_square_root(x: float) -> float: + if x < 0: + push_error(ERROR_NEGATIVE_VALUE) + return 0 + return sqrt(x) + + +static func get_capitalized_text(text: String) -> String: + var capitalized := text.capitalize() + if capitalized == text: + push_warning(WARN_UNCHANGED) + return capitalized diff --git a/addons/gdUnit4/test/core/execution/resources/error_monitoring/TestSuiteWithPushErrorTests.gd b/addons/gdUnit4/test/core/execution/resources/error_monitoring/TestSuiteWithPushErrorTests.gd new file mode 100644 index 0000000..f924489 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/error_monitoring/TestSuiteWithPushErrorTests.gd @@ -0,0 +1,35 @@ +extends GdUnitTestSuite + + +@onready var ExampleClassWithPushNotifications := preload("res://addons/gdUnit4/test/core/execution/resources/error_monitoring/ExampleClassWithPushNotifications.gd") + +func test_without_push_errors() -> void: + # should allways succeed + assert_float(ExampleClassWithPushNotifications.get_square_root(25)).is_equal_approx(5, 1e-5) + + +func test_with_push_error() -> void: + # should only failing when push_error reporting is activated + ExampleClassWithPushNotifications.get_square_root(-1) + + +func test_with_push_error_is_catched() -> void: + # should allways succeed, because it is catched by the assert_error + assert_error(func() -> void: ExampleClassWithPushNotifications.get_square_root(-1)) \ + .is_push_error(ExampleClassWithPushNotifications.ERROR_NEGATIVE_VALUE) + + +func test_without_push_warning() -> void: + # should allways succeed + assert_str(ExampleClassWithPushNotifications.get_capitalized_text("T_ext")).is_equal("T Ext") + + +func test_with_push_warning() -> void: + # should only failing when push_error reporting is activated + assert_str(ExampleClassWithPushNotifications.get_capitalized_text("Text")).is_equal("Text") + + +func test_push_warning_is_catched() -> void: + # should allways succeed, because it is catched by the assert_error + assert_error(func() -> void: ExampleClassWithPushNotifications.get_capitalized_text("Text")) \ + .is_push_warning(ExampleClassWithPushNotifications.WARN_UNCHANGED) diff --git a/addons/gdUnit4/test/core/execution/resources/orphans/.gdignore b/addons/gdUnit4/test/core/execution/resources/orphans/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/core/execution/resources/orphans/OrphanTest.gd b/addons/gdUnit4/test/core/execution/resources/orphans/OrphanTest.gd new file mode 100644 index 0000000..181d51e --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/orphans/OrphanTest.gd @@ -0,0 +1,47 @@ +extends GdUnitTestSuite + +@warning_ignore_start("unused_private_class_variable") +var _member_node1 := T2.new() # produces an orphan Node2D + + +func after() -> void: + await get_tree().process_frame + collect_orphan_node_details() + + +func _after_test() -> void: + collect_orphan_node_details() + + +func test_orphans2() -> void: + var _func_ref2 := RefCounted.new() # is refcounted and never orphan + var _func_obj2 := Object.new() # produces an orphan Object + var _func_node2 := Node3D.new() # produces an orphan Node3D + collect_orphan_node_details() + + +func test_orphans3() -> void: + var _func_node3 := Node3D.new() # produces an orphan Node3D + var t2 := T2.new() + add_child(t2) + collect_orphan_node_details() + + +func test_with_scene_orphans() -> void: + # run scene with orphan nodes + var runner := scene_runner("res://addons/gdUnit4/test/core/execution/resources/OrphanScene.tscn") + @warning_ignore("redundant_await") + await runner.simulate_frames(10) + collect_orphan_node_details() + + +func test_load_scene_orphans() -> void: + # run scene with orphan nodes + var _scene :Node2D = preload("res://addons/gdUnit4/test/core/execution/resources/OrphanScene.tscn").instantiate() + @warning_ignore("redundant_await") + collect_orphan_node_details() + + +func _test_no_orphans() -> void: + var _func_node2 :Node3D = auto_free(Node3D.new()) + collect_orphan_node_details() diff --git a/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteFailAndOrpahnsDetected.gd b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteFailAndOrpahnsDetected.gd new file mode 100644 index 0000000..08e5980 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteFailAndOrpahnsDetected.gd @@ -0,0 +1,51 @@ +# this test suite fails on multiple stages and detects orphans +extends GdUnitTestSuite + + +var _orphans: Array[Node] = [] +@warning_ignore("untyped_declaration") +var before_n1 + +func before() -> void: + # create a node where never freed (orphan) + before_n1 = Node.new() + _orphans.append(before_n1) + + +func before_test() -> void: + # create two node where never freed (orphan) + var before_test_n1 := Node.new() + var before_test_n2 := Node.new() + _orphans.append_array([before_test_n1, before_test_n2]) + + +# ends with warning and 3 orphan detected +func test_case1() -> void: + # create three node where never freed (orphan) + var n11 := Node.new() + var n12 := Node.new() + var n13 := Node.new() + _orphans.append_array([n11, n12, n13]) + await get_tree().process_frame + collect_orphan_node_details() + + +# ends with error and 4 orphan detected +func test_case2() -> void: + # create four node where never freed (orphan) + var n21 := Node.new() + var n22 := Node.new() + var n23 := Node.new() + var n24 := Node.new() + _orphans.append_array([n21, n22, n23, n24]) + fail("faild on test_case2()") + await get_tree().process_frame + collect_orphan_node_details() + + +# we manually freeing the orphans from the simulated testsuite to prevent memory leaks here +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + for orphan in _orphans: + orphan.free() + _orphans.clear() diff --git a/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase1.gd b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase1.gd new file mode 100644 index 0000000..c9aefcd --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase1.gd @@ -0,0 +1,26 @@ +extends GdUnitTestSuite + + +func test_no_orphans() -> void: + # Create an orphan node with using auto_free tool. + var _node1: Node3D = auto_free(Node3D.new()) + + # Create an orphan node and release it manually. + var _node2 := Node3D.new() + _node2.free() + + +func test_orphans_one() -> void: + # Create an orphan node + var _node := Node3D.new() # produces an orphan Node3D + + # We adding this to collect details about + collect_orphan_node_details() + + +func test_orphans_two() -> void: + # Create an orphan node + var _node := Node3D.new() + + # We adding this to collect details about + collect_orphan_node_details() diff --git a/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase2.gd b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase2.gd new file mode 100644 index 0000000..428fd34 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase2.gd @@ -0,0 +1,39 @@ +extends GdUnitTestSuite + +@warning_ignore_start("unused_private_class_variable") +# Create an orphan node +var _member_node := Node3D.new() +@warning_ignore("untyped_declaration") +var _before_untyped +var _before_with_type: Node + + +func before() -> void: + _before_untyped = Node.new() + _before_with_type = Node.new() + + +func test_no_orphans_at_function() -> void: + # Create an orphan node with using auto_free tool. + var _node1 :Node3D = auto_free(Node3D.new()) + + # Create an orphan node and release it manually. + var _node2 := Node3D.new() + _node2.free() + + +func test_orphans_at_function_without_details() -> void: + # Create an orphan node + var _node := Node3D.new() # produces an orphan Node3D + + # We adding this to collect details about + collect_orphan_node_details() + + +func test_orphans_at_function_with_details() -> void: + # Create an orphan node + @warning_ignore("untyped_declaration") + var _node = Node3D.new() + + # We adding this to collect details about + collect_orphan_node_details() diff --git a/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase3.gd b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase3.gd new file mode 100644 index 0000000..6a8f087 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteWithOrphansCase3.gd @@ -0,0 +1,33 @@ +# This test should detect orphan nodes created at 'before_test' hook +extends GdUnitTestSuite + + +var _member_node1: Node3D # will be orphan +var _member_node2: Node3D +var _member_node3: Node3D + + +func before_test() -> void: + # Should be detected as an orhan because is never freed + _member_node1 = Node3D.new() + # Using auto_free should freeing the node by default + _member_node2 = auto_free(Node3D.new()) + # Do manually freeing at 'after_test' + _member_node3 = Node3D.new() + + +func after_test() -> void: + # Do manual cleanup for node3 + _member_node3.free() + + +func after() -> void: + assert_that(_member_node1).is_not_null() + assert_that(_member_node2).is_null() + assert_that(_member_node3).is_null() + + +func test_members() -> void: + assert_that(_member_node1).is_not_null() + assert_that(_member_node2).is_not_null() + assert_that(_member_node3).is_not_null() diff --git a/addons/gdUnit4/test/core/execution/resources/t2.gd b/addons/gdUnit4/test/core/execution/resources/t2.gd new file mode 100644 index 0000000..254b294 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/t2.gd @@ -0,0 +1,10 @@ +class_name T2 +extends Node2D + +var t3 := T3.new() + +func _ready() -> void: + set_name("T2") + # This node is tracked as orphan but we can not collect details from the script_backtraces + var _x2 := Node2D.new() + prints("_x2", _x2.get_instance_id()) diff --git a/addons/gdUnit4/test/core/execution/resources/t2.gd.uid b/addons/gdUnit4/test/core/execution/resources/t2.gd.uid new file mode 100644 index 0000000..d31bedf --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/t2.gd.uid @@ -0,0 +1 @@ +uid://dnab5hrtlvb80 diff --git a/addons/gdUnit4/test/core/execution/resources/t3.gd b/addons/gdUnit4/test/core/execution/resources/t3.gd new file mode 100644 index 0000000..1ce30eb --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/t3.gd @@ -0,0 +1,9 @@ +class_name T3 +extends Node3D + +var x31 := Node2D.new() + +func _ready() -> void: + set_name("T3") + var _x3 := Node2D.new() + # add_child(_x2) diff --git a/addons/gdUnit4/test/core/execution/resources/t3.gd.uid b/addons/gdUnit4/test/core/execution/resources/t3.gd.uid new file mode 100644 index 0000000..554db88 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/resources/t3.gd.uid @@ -0,0 +1 @@ +uid://bn07bv6uwjdyi diff --git a/addons/gdUnit4/test/core/flaky_test.gd b/addons/gdUnit4/test/core/flaky_test.gd new file mode 100644 index 0000000..ac68df6 --- /dev/null +++ b/addons/gdUnit4/test/core/flaky_test.gd @@ -0,0 +1,145 @@ +extends GdUnitTestSuite + + +var test_retries := { + "test_flaky_success" = 0, + "test_flaky_fail" = 0, + "test_success" = 0, + "test_paramaterized_flaky:0" = 0, + "test_paramaterized_flaky:1" = 0, + "test_paramaterized_flaky:2" = 0, + "test_paramaterized_flaky:3" = 0, + "test_paramaterized_flaky:4" = 0, + "test_paramaterized_flaky:5" = 0, + "test_fuzzed_flaky_success" = 0, + "test_fuzzed_flaky_fail" = 0 +} + +var _run_with_reries := 5 + + +class ValueSetFuzzer extends Fuzzer: + var _values := [0,1,2,3,4] + + func next_value() -> Variant: + return _values.pop_front() + + +func before(_do_skip := true, _skip_reason := "Do only activate for internal testing!") -> void: + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_CHECK, true) + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_MAX_RETRIES, _run_with_reries) + + +func after() -> void: + var retry_count: int = test_retries["test_flaky_success"] + assert_int(retry_count)\ + .override_failure_message("Expecting 3 retries to succeed for test 'test_flaky_success'\n but was %d" % retry_count)\ + .is_equal(3) + retry_count = test_retries["test_flaky_fail"] + assert_int(retry_count)\ + .override_failure_message("Expecting %d retries for test 'test_flaky_fail'\n but was %d" % [_run_with_reries, retry_count])\ + .is_equal(_run_with_reries) + retry_count = test_retries["test_success"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_success'\n but was %d" % retry_count)\ + .is_equal(1) + # verify retry count of paramaterized test + retry_count = test_retries["test_paramaterized_flaky:0"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_paramaterized_flaky:0'\n but was %d" % retry_count)\ + .is_equal(1) + retry_count = test_retries["test_paramaterized_flaky:1"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_paramaterized_flaky:1'\n but was %d" % retry_count)\ + .is_equal(1) + retry_count = test_retries["test_paramaterized_flaky:2"] + assert_int(retry_count)\ + .override_failure_message("Expecting %d test iteration to fail 'test_paramaterized_flaky:2'\n but was %d" % [_run_with_reries, retry_count])\ + .is_equal(_run_with_reries) + retry_count = test_retries["test_paramaterized_flaky:3"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_paramaterized_flaky:3'\n but was %d" % retry_count)\ + .is_equal(1) + retry_count = test_retries["test_paramaterized_flaky:4"] + assert_int(retry_count)\ + .override_failure_message("Expecting 3 test iteration to succeed 'test_paramaterized_flaky:4'\n but was %d" % retry_count)\ + .is_equal(3) + # fuzzed tests + retry_count = test_retries["test_fuzzed_flaky_success"] + assert_int(retry_count)\ + .override_failure_message("Expecting 3 retries to succeed for test 'test_fuzzed_flaky_success'\n but was %d" % retry_count)\ + .is_equal(3) + retry_count = test_retries["test_fuzzed_flaky_fail"] + assert_int(retry_count)\ + .override_failure_message("Expecting %d retries for test 'test_fuzzed_flaky_fail'\n but was %d" % [_run_with_reries, retry_count])\ + .is_equal(_run_with_reries) + + +func test_flaky_success() -> void: + test_retries["test_flaky_success"] += 1 + var retry_count: int = test_retries["test_flaky_success"] + # do retry between 1 and 3 + assert_int(retry_count).is_less_equal(3) + if retry_count <= 2: + fail("failure 1: failed at retry %d" % retry_count) + fail("failure 2: failed at retry %d" % retry_count) + + +func test_flaky_fail() -> void: + test_retries["test_flaky_fail"] += 1 + var retry_count: int = test_retries["test_flaky_fail"] + # do retry between 1 and 5 + assert_int(retry_count).is_less_equal(6) + if retry_count < 6: + fail("failed on test retry %d" % retry_count) + + +func test_success() -> void: + test_retries["test_success"] += 1 + var retry_count: int = test_retries["test_success"] + # do retry only one time + assert_int(retry_count).is_equal(1) + assert_bool(true).is_true() + + +func test_paramaterized_flaky(test_index: int, expected_retry_count: int, _test_parameters := [ + [0, 1], + [1, 1], + [2, 6], + [3, 1], + [4, 3]]) -> void: + + var test_case_name := "test_paramaterized_flaky:%d" % test_index + test_retries[test_case_name] += 1 + var retry_count: int = test_retries[test_case_name] + assert_int(retry_count).is_less_equal(expected_retry_count) + + if test_index == 2 or test_index == 4: + # do fail if retry_count less expected count to fail + if retry_count < expected_retry_count: + fail("failed at retry %d" % retry_count) + + +func test_fuzzed_flaky_success(fuzzer := ValueSetFuzzer.new(), _fuzzer_iterations := 5) -> void: + var fuzzer_value: int = fuzzer.next_value() + if fuzzer_value == 0: + test_retries["test_fuzzed_flaky_success"] += 1 + var retry_count :int = test_retries["test_fuzzed_flaky_success"] + # do retry between 1 and 3 + assert_int(retry_count).is_less_equal(3) + + if retry_count <= 2: + fail("failure 1: failed at retry %d" % retry_count) + fail("failure 2: failed at retry %d" % retry_count) + + +func test_fuzzed_flaky_fail(fuzzer := ValueSetFuzzer.new(), _fuzzer_iterations := 5) -> void: + var fuzzer_value: int = fuzzer.next_value() + if fuzzer_value == 0: + test_retries["test_fuzzed_flaky_fail"] += 1 + var retry_count :int = test_retries["test_fuzzed_flaky_fail"] + # do retry between 1 and 3 + assert_int(retry_count).is_less_equal(6) + + if retry_count < 6: + fail("failed at retry %d" % retry_count) diff --git a/addons/gdUnit4/test/core/flaky_test.gd.uid b/addons/gdUnit4/test/core/flaky_test.gd.uid new file mode 100644 index 0000000..b7b7a24 --- /dev/null +++ b/addons/gdUnit4/test/core/flaky_test.gd.uid @@ -0,0 +1 @@ +uid://wlktjixhaco6 diff --git a/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd new file mode 100644 index 0000000..24e1564 --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd @@ -0,0 +1,23 @@ +class_name ExampleTestSessionHookA +extends GdUnitTestSessionHook + +signal start_up(name: String) +signal shut_down(name: String) + +var _state: PackedStringArray = [] + + +func _init() -> void: + super("hook_a", "An example hook for testing purpose.") + + +func startup(_session: GdUnitTestSession) -> GdUnitResult: + start_up.emit(name) + _state.push_back("startup") + return GdUnitResult.success() + + +func shutdown(_session: GdUnitTestSession) -> GdUnitResult: + shut_down.emit(name) + _state.push_back("shutdown") + return GdUnitResult.success() diff --git a/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd.uid b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd.uid new file mode 100644 index 0000000..9b58384 --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd.uid @@ -0,0 +1 @@ +uid://dpjb8x4h0ccu4 diff --git a/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookB.gd b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookB.gd new file mode 100644 index 0000000..e1560ab --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookB.gd @@ -0,0 +1,23 @@ +class_name ExampleTestSessionHookB +extends GdUnitTestSessionHook + +signal start_up(name: String) +signal shut_down(name: String) + +var _state: PackedStringArray = [] + + +func _init() -> void: + super("hook_b", "An example hook for testing purpose.") + + +func startup(_session: GdUnitTestSession) -> GdUnitResult: + start_up.emit(name) + _state.push_back("startup") + return GdUnitResult.success() + + +func shutdown(_session: GdUnitTestSession) -> GdUnitResult: + shut_down.emit(name) + _state.push_back("shutdown") + return GdUnitResult.success() diff --git a/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookB.gd.uid b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookB.gd.uid new file mode 100644 index 0000000..556b127 --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookB.gd.uid @@ -0,0 +1 @@ +uid://ule6ueii6mpf diff --git a/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookC.gd b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookC.gd new file mode 100644 index 0000000..9437b91 --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookC.gd @@ -0,0 +1,23 @@ +class_name ExampleTestSessionHookC +extends GdUnitTestSessionHook + +signal start_up(name: String) +signal shut_down(name: String) + +var _state: PackedStringArray = [] + + +func _init() -> void: + super("hook_c", "An example hook for testing purpose.") + + +func startup(_session: GdUnitTestSession) -> GdUnitResult: + start_up.emit(name) + _state.push_back("startup") + return GdUnitResult.success() + + +func shutdown(_session: GdUnitTestSession) -> GdUnitResult: + shut_down.emit(name) + _state.push_back("shutdown") + return GdUnitResult.success() diff --git a/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookC.gd.uid b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookC.gd.uid new file mode 100644 index 0000000..aa2c9dc --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/ExampleTestSessionHookC.gd.uid @@ -0,0 +1 @@ +uid://lqvfqo6xkrqa diff --git a/addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd b/addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd new file mode 100644 index 0000000..03c4865 --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd @@ -0,0 +1,149 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + + +func hook_service() -> GdUnitTestSessionHookService: + return auto_free(GdUnitTestSessionHookService.new()) + + +func test_load_hook_not_exists() -> void: + var hook := hook_service().load_hook("res://addons/gdUnit4/test/core/hooks/InvalidTestSessionHook.gd") + + assert_result(hook)\ + .is_error()\ + .contains_message("The hook 'res://addons/gdUnit4/test/core/hooks/InvalidTestSessionHook.gd' not exists.") + + +func test_load_hook_not_inherits_hook() -> void: + var hook := hook_service().load_hook("res://addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd") + + assert_result(hook)\ + .is_error()\ + .contains_message("The hook 'res://addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd' must inhertit from 'GdUnitTestSessionHook'.") + + +func test_load_hook_success() -> void: + var hook := hook_service().load_hook("res://addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd") + + assert_result(hook).is_success() + assert_object(hook.value())\ + .is_not_null()\ + .is_inheriting(GdUnitTestSessionHook) + + +func test_register_hook() -> void: + var service := hook_service() + + var hook := ExampleTestSessionHookA.new() + service.register(hook) + + # verify + assert_array(service.enigne_hooks).contains_exactly([hook]) + + # try to register at twice should fail + var result := service.register(hook) + assert_result(result)\ + .is_error()\ + .contains_message("A hook instance of 'res://addons/gdUnit4/test/core/hooks/ExampleTestSessionHookA.gd' is already registered.") + assert_array(service.enigne_hooks).contains_exactly([hook]) + + +func test_execute_startup() -> void: + var service := hook_service() + + var hook := ExampleTestSessionHookA.new() + service.register(hook) + + assert_array(hook._state).is_empty() + + service.execute_startup(GdUnitTestSession.new([], "res://reports")) + assert_array(hook._state).contains_exactly(["startup"]) + + +func test_execute_shutdown() -> void: + var service := hook_service() + + var hook := ExampleTestSessionHookA.new() + service.register(hook) + + assert_array(hook._state).is_empty() + + service.execute_shutdown(GdUnitTestSession.new([], "res://reports")) + assert_array(hook._state).contains_exactly(["shutdown"]) + + +func test_hook_priority_execution() -> void: + var service := hook_service() + var hook_a := ExampleTestSessionHookA.new() + var hook_b := ExampleTestSessionHookB.new() + var hook_c := ExampleTestSessionHookC.new() + + var start_up_called := PackedStringArray() + hook_a.start_up.connect(func(hook_name: String) -> void: start_up_called.push_back(hook_name)) + hook_b.start_up.connect(func(hook_name: String) -> void: start_up_called.push_back(hook_name)) + hook_c.start_up.connect(func(hook_name: String) -> void: start_up_called.push_back(hook_name)) + var shutdown_called := PackedStringArray() + hook_a.shut_down.connect(func(hook_name: String) -> void: shutdown_called.push_back(hook_name)) + hook_b.shut_down.connect(func(hook_name: String) -> void: shutdown_called.push_back(hook_name)) + hook_c.shut_down.connect(func(hook_name: String) -> void: shutdown_called.push_back(hook_name)) + + # register order is important + assert_result(service.register(hook_b)).is_success() + assert_result(service.register(hook_a)).is_success() + assert_result(service.register(hook_c)).is_success() + assert_array(service.enigne_hooks).contains_exactly([hook_b, hook_a, hook_c]) + + # must be executed ordered + var test_session := GdUnitTestSession.new([], "res://reports") + assert_result(await service.execute_startup(test_session)).is_success() + assert_result(await service.execute_shutdown(test_session)).is_success() + + # verify is called in the correct order + assert_array(start_up_called).contains_exactly(["hook_b", "hook_a", "hook_c"]) + assert_array(shutdown_called).contains_exactly(["hook_c", "hook_a", "hook_b"]) + + +func test_move_before() -> void: + var service := hook_service() + var hook_a := ExampleTestSessionHookA.new() + var hook_b := ExampleTestSessionHookB.new() + var hook_c := ExampleTestSessionHookC.new() + + service.register(hook_a) + service.register(hook_b) + service.register(hook_c) + + assert_array(service.enigne_hooks).contains_exactly([hook_a, hook_b, hook_c]) + + # Verify move hook_b before hook_a + service.move_before(hook_b, hook_a) + assert_array(service.enigne_hooks).contains_exactly([hook_b, hook_a, hook_c]) + # Call at twice to verify the order is not changed because it is already before + service.move_before(hook_b, hook_a) + assert_array(service.enigne_hooks).contains_exactly([hook_b, hook_a, hook_c]) + # Verify move hook_b before hook_c + service.move_before(hook_c, hook_b) + assert_array(service.enigne_hooks).contains_exactly([hook_c, hook_b, hook_a]) + + +func test_move_after() -> void: + var service := hook_service() + var hook_a := ExampleTestSessionHookA.new() + var hook_b := ExampleTestSessionHookB.new() + var hook_c := ExampleTestSessionHookC.new() + + service.register(hook_a) + service.register(hook_b) + service.register(hook_c) + + assert_array(service.enigne_hooks).contains_exactly([hook_a, hook_b, hook_c]) + + # Verify move hook_a after hook_b + service.move_after(hook_a, hook_b) + assert_array(service.enigne_hooks).contains_exactly([hook_b, hook_a, hook_c]) + # Call at twice to verify the order is not changed because it is already after + service.move_after(hook_a, hook_b) + assert_array(service.enigne_hooks).contains_exactly([hook_b, hook_a, hook_c]) + # Verify move hook_b after hook_c + service.move_after(hook_b, hook_c) + assert_array(service.enigne_hooks).contains_exactly([hook_a, hook_c, hook_b]) diff --git a/addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd.uid b/addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd.uid new file mode 100644 index 0000000..f19e0a3 --- /dev/null +++ b/addons/gdUnit4/test/core/hooks/GdUnitTestSessionHookServiceTest.gd.uid @@ -0,0 +1 @@ +uid://cuijymsbrtr8k diff --git a/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd b/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd new file mode 100644 index 0000000..4cd39d5 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd @@ -0,0 +1,246 @@ +# GdUnit generated TestSuite +class_name GdDefaultValueDecoderTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd' + + +var _tested_types := {} + + +func after() -> void: + # we verify we have covered all variant types + for type_id in TYPE_MAX: + if type_id == TYPE_OBJECT: + continue + @warning_ignore("unsafe_method_access") + assert_that(_tested_types.get(type_id))\ + .override_failure_message("Missing Variant type '%s'" % GdObjects.type_as_string(type_id))\ + .is_not_null() + + +func test_decode_Primitives(variant_type: int, value: Variant, expected: String, _test_parameters := [ + [TYPE_NIL, null, "null"], + [TYPE_BOOL, true, "true"], + [TYPE_BOOL, false, "false"], + [TYPE_INT, -100, "-100"], + [TYPE_INT, 0, "0"], + [TYPE_INT, 100, "100"], + [TYPE_FLOAT, -100.123, "-100.123000"], + [TYPE_FLOAT, 0.00, "0.000000"], + [TYPE_FLOAT, 100, "100.000000"], + [TYPE_FLOAT, 100.123, "100.123000"], + [TYPE_STRING, "hello", '"hello"'], + [TYPE_STRING, "", '""'], + [TYPE_STRING_NAME, StringName("hello"), 'StringName("hello")'], + [TYPE_STRING_NAME, StringName(""), 'StringName()'], + ]) -> void: + + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Vectors(variant_type: int, value: Variant, expected: String, _test_parameters := [ + [TYPE_VECTOR2, Vector2(), "Vector2()"], + [TYPE_VECTOR2, Vector2(1,2), "Vector2" + str(Vector2(1, 2))], + [TYPE_VECTOR2I, Vector2i(), "Vector2i()"], + [TYPE_VECTOR2I, Vector2i(1,2), "Vector2i(1, 2)"], + [TYPE_VECTOR3, Vector3(), "Vector3()"], + [TYPE_VECTOR3, Vector3(1,2,3), "Vector3" +str(Vector3(1, 2, 3))], + [TYPE_VECTOR3I, Vector3i(), "Vector3i()"], + [TYPE_VECTOR3I, Vector3i(1,2,3), "Vector3i(1, 2, 3)"], + [TYPE_VECTOR4, Vector4(), "Vector4()"], + [TYPE_VECTOR4, Vector4(1,2,3,4), "Vector4" + str(Vector4(1, 2, 3, 4))], + [TYPE_VECTOR4I, Vector4i(), "Vector4i()"], + [TYPE_VECTOR4I, Vector4i(1,2,3,4), "Vector4i(1, 2, 3, 4)"], + ]) -> void: + + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Rect2(variant_type: int, value: Variant, expected: String, _test_parameters := [ + [TYPE_RECT2, Rect2(), "Rect2()"], + [TYPE_RECT2, Rect2(1,2, 10,20), "Rect2(Vector2"+str(Vector2(1, 2))+", Vector2"+str(Vector2(10, 20))+")"], + [TYPE_RECT2I, Rect2i(), "Rect2i()"], + [TYPE_RECT2I, Rect2i(1,2, 10,20), "Rect2i(Vector2i(1, 2), Vector2i(10, 20))"], + ]) -> void: + + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Transforms(variant_type: int, value: Variant, expected: String, _test_parameters := [ + [TYPE_TRANSFORM2D, Transform2D(), + "Transform2D()"], + [TYPE_TRANSFORM2D, Transform2D(2.0, Vector2(1,2)), + "Transform2D(Vector2(-0.416147, 0.909297), Vector2(-0.909297, -0.416147), Vector2"+str(Vector2(1, 2))+")"], + [TYPE_TRANSFORM2D, Transform2D(2.0, Vector2(1,2), 2.0, Vector2(3,4)), + "Transform2D(Vector2(-0.416147, 0.909297), Vector2(1.513605, -1.307287), Vector2"+str(Vector2(3, 4))+")"], + [TYPE_TRANSFORM2D, Transform2D(Vector2(1,2), Vector2(3,4), Vector2.ONE), + "Transform2D(Vector2"+str(Vector2(1, 2))+", Vector2"+str(Vector2(3, 4))+", Vector2"+str(Vector2(1, 1))+")"], + [TYPE_TRANSFORM3D, Transform3D(), + "Transform3D()"], + [TYPE_TRANSFORM3D, Transform3D(Basis.FLIP_X, Vector3.ONE), + "Transform3D(Vector3"+str(Vector3(-1, 0, 0))+", Vector3"+str(Vector3(0, 1, 0)) + +", Vector3"+str(Vector3(0, 0, 1))+", Vector3"+str(Vector3(1, 1, 1))+")"], + [TYPE_TRANSFORM3D, Transform3D(Vector3(1,2,3), Vector3(4,5,6), Vector3(7,8,9), Vector3.ONE), + "Transform3D(Vector3"+str(Vector3(1, 2, 3))+", Vector3"+str(Vector3(4, 5, 6)) + +", Vector3"+str(Vector3(7, 8, 9))+", Vector3"+str(Vector3(1, 1, 1))+")"], + [TYPE_PROJECTION, Projection(), "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" + % [Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0), Vector4(0, 0, 0, 1)]], + [TYPE_PROJECTION, Projection(Vector4.ONE, Vector4.ONE*2, Vector4.ONE*3, Vector4.ZERO), + "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % + [Vector4.ONE, Vector4.ONE*2, Vector4.ONE*3, Vector4.ZERO]] + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Plane(variant_type: int, value: Plane, expected: String, _test_parameters := [ + [TYPE_PLANE, Plane(), "Plane()"], + [TYPE_PLANE, Plane(1,2,3,4), "Plane(1, 2, 3, 4)"], + [TYPE_PLANE, Plane(Vector3.ONE, Vector3.ZERO), "Plane(1, 1, 1, 0)"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Quaternion(variant_type: int, value: Quaternion, expected: String, _test_parameters := [ + [TYPE_QUATERNION, Quaternion(), "Quaternion()"], + [TYPE_QUATERNION, Quaternion(1,2,3,4), "Quaternion(1, 2, 3, 4)"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_AABB(variant_type: int, value: AABB, expected: String, _test_parameters := [ + [TYPE_AABB, AABB(), "AABB()"], + [TYPE_AABB, AABB(Vector3.ONE, Vector3(10,20,30)), "AABB(Vector3"+str(Vector3(1, 1, 1))+", Vector3"+str(Vector3(10, 20, 30))+")"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Basis(variant_type: int, value: Basis, expected: String, _test_parameters := [ + [TYPE_BASIS, Basis(), "Basis()"], + [TYPE_BASIS, Basis(Vector3(0.1,0.2,0.3).normalized(), .1), + "Basis(Vector3(0.995361, 0.080758, -0.052293), Vector3(-0.079331, 0.996432, 0.028823), Vector3(0.054434, -0.024541, 0.998216))"], + [TYPE_BASIS, Basis(Vector3.ONE, Vector3.ONE*2, Vector3.ONE*3), + "Basis(Vector3"+str(Vector3(1, 1, 1))+", Vector3"+str(Vector3(2, 2, 2))+", Vector3"+str(Vector3(3, 3, 3))+")"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Color(variant_type: int, value: Color, expected: String, _test_parameters := [ + [TYPE_COLOR, Color(), "Color()"], + [TYPE_COLOR, Color.RED, "Color"+str(Color(1, 0, 0, 1))], + [TYPE_COLOR, Color(1,.2,.5,.5), "Color"+str(Color(1, 0.2, 0.5, 0.5))], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_NodePath(variant_type: int, value: NodePath, expected: String, _test_parameters := [ + [TYPE_NODE_PATH, NodePath(), 'NodePath()'], + [TYPE_NODE_PATH, NodePath("/foo/bar"), 'NodePath("/foo/bar")'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_RID(variant_type: int, value: RID, expected: String, _test_parameters := [ + [TYPE_RID, RID(), 'RID()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func _test_decode_Object(variant_type: int, value: Node, expected: String, _test_parameters := [ + [TYPE_OBJECT, Node.new(), 'Node.new()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Callable(variant_type: int, value: Callable, expected: String, _test_parameters := [ + [TYPE_CALLABLE, Callable(), 'Callable()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Signal(variant_type: int, value: Signal, expected: String, _test_parameters := [ + [TYPE_SIGNAL, Signal(), 'Signal()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Dictionary(variant_type: int, value: Dictionary, expected: String, _test_parameters := [ + [TYPE_DICTIONARY, {}, '{}'], + [TYPE_DICTIONARY, Dictionary(), '{}'], + [TYPE_DICTIONARY, {1:2, 2:3}, '{ 1: 2, 2: 3 }'], + [TYPE_DICTIONARY, {"aa":2, "bb":"cc"}, '{ "aa": 2, "bb": "cc" }'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_Array(variant_type: int, value: Array, expected: String, _test_parameters := [ + [TYPE_ARRAY, [], '[]'], + [TYPE_ARRAY, Array(), '[]'], + [TYPE_ARRAY, [1,2,3], '[1, 2, 3]'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +func test_decode_typedArrays(variant_type: int, value: Variant, expected: String, _test_parameters := [ + [TYPE_PACKED_BYTE_ARRAY, PackedByteArray(), + 'PackedByteArray()'], + [TYPE_PACKED_BYTE_ARRAY, PackedByteArray([1, 2, 3]), + 'PackedByteArray([1, 2, 3])'], + [TYPE_PACKED_COLOR_ARRAY, PackedColorArray(), + 'PackedColorArray()'], + [TYPE_PACKED_COLOR_ARRAY, PackedColorArray([Color.RED, Color.BLUE]), + 'PackedColorArray([Color'+str(Color(1, 0, 0, 1))+', Color'+str(Color(0, 0, 1, 1))+'])'], + [TYPE_PACKED_FLOAT32_ARRAY, PackedFloat32Array(), + 'PackedFloat32Array()'], + [TYPE_PACKED_FLOAT32_ARRAY, PackedFloat32Array([1.2, 2.3]), + 'PackedFloat32Array([1.20000004768372, 2.29999995231628])'], + [TYPE_PACKED_FLOAT64_ARRAY, PackedFloat64Array(), + 'PackedFloat64Array()'], + [TYPE_PACKED_FLOAT64_ARRAY, PackedFloat64Array([1.2, 2.3]), + 'PackedFloat64Array([1.2, 2.3])'], + [TYPE_PACKED_INT32_ARRAY, PackedInt32Array(), + 'PackedInt32Array()'], + [TYPE_PACKED_INT32_ARRAY, PackedInt32Array([1, 2]), + 'PackedInt32Array([1, 2])'], + [TYPE_PACKED_INT64_ARRAY, PackedInt64Array(), + 'PackedInt64Array()'], + [TYPE_PACKED_INT64_ARRAY, PackedInt64Array([1, 2]), + 'PackedInt64Array([1, 2])'], + [TYPE_PACKED_STRING_ARRAY, PackedStringArray(), + 'PackedStringArray()'], + [TYPE_PACKED_STRING_ARRAY, PackedStringArray(["aa", "bb"]), + 'PackedStringArray(["aa", "bb"])'], + [TYPE_PACKED_VECTOR2_ARRAY, PackedVector2Array(), + 'PackedVector2Array()'], + [TYPE_PACKED_VECTOR2_ARRAY, PackedVector2Array([Vector2.ONE, Vector2.ONE*2]), + 'PackedVector2Array([Vector2'+str(Vector2.ONE)+', Vector2'+str(Vector2.ONE*2)+'])'], + [TYPE_PACKED_VECTOR3_ARRAY, PackedVector3Array(), + 'PackedVector3Array()'], + [TYPE_PACKED_VECTOR3_ARRAY, PackedVector3Array([Vector3.ONE, Vector3.ONE*2]), + 'PackedVector3Array([Vector3'+str(Vector3.ONE)+', Vector3'+str(Vector3.ONE*2)+'])'], + [TYPE_PACKED_VECTOR4_ARRAY, PackedVector4Array(), + 'PackedVector4Array()'], + [TYPE_PACKED_VECTOR4_ARRAY, PackedVector4Array([Vector4.ONE, Vector4.ONE*2]), + 'PackedVector4Array([Vector4'+str(Vector4.ONE)+', Vector4'+str(Vector4.ONE*2)+'])'] + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 diff --git a/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd.uid b/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd.uid new file mode 100644 index 0000000..ad69b9e --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd.uid @@ -0,0 +1 @@ +uid://c0qbu4lnl1uxp diff --git a/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd b/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd new file mode 100644 index 0000000..1c9d6c4 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd @@ -0,0 +1,125 @@ +# GdUnit generated TestSuite +class_name GdFunctionArgumentTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdFunctionArgument.gd' + + +func test__parse_argument_as_array_typ1() -> void: + var test_parameters := """[ + [1, "flowchart TD\nid>This is a flag shaped node]"], + [ + 2, + "flowchart TD\nid(((This is a\tdouble circle node)))" + ], + [3, + "flowchart TD\nid((This is a circular node))"], + [ + 4, "flowchart TD\nid>This is a flag shaped node]" + ], + [5, "flowchart TD\nid{'This is a rhombus node'}"], + [6, 'flowchart TD\nid((This is a circular node))'], + [7, 'flowchart TD\nid>This is a flag shaped node]'], [8, 'flowchart TD\nid{"This is a rhombus node"}'], + [9, \"\"\" + flowchart TD + id{"This is a rhombus node"} + \"\"\"] + ]""" + + var fa := GdFunctionArgument.new("test_parameters", TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).contains_exactly([ + """[1, "flowchart TDid>This is a flag shaped node]"]""", + """[2, "flowchart TDid(((This is a\tdouble circle node)))"]""", + """[3, "flowchart TDid((This is a circular node))"]""", + """[4, "flowchart TDid>This is a flag shaped node]"]""", + """[5, "flowchart TDid{'This is a rhombus node'}"]""", + """[6, 'flowchart TDid((This is a circular node))']""", + """[7, 'flowchart TDid>This is a flag shaped node]']""", + """[8, 'flowchart TDid{"This is a rhombus node"}']""", + """[9, \"\"\"flowchart TDid{"This is a rhombus node"}\"\"\"]""" + ] + ) + + +func test__parse_argument_as_array_typ2() -> void: + var test_parameters := """[ + ["test_a", null, "LOG", {}], + [ + "test_b", + Node2D, + null, + {Node2D: "ER,ROR"} + ], + [ + "test_c", + Node2D, + "LOG", + {Node2D: "LOG"} + ] + ]""" + var fa := GdFunctionArgument.new("test_parameters", TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).contains_exactly([ + """["test_a", null, "LOG", {}]""", + """["test_b", Node2D, null, {Node2D: "ER,ROR"}]""", + """["test_c", Node2D, "LOG", {Node2D: "LOG"}]""" + ] + ) + + +func test__parse_argument_as_array_bad_formatted() -> void: + var test_parameters := """[ + ["test_a", null, "LOG", {}], + [ + "test_b", + Node2D, + null, + {Node2D: "ER,ROR"} + ], + [ + "test_c", + Node2D, + "LOG", + {Node2D: "LOG 1"} + ] + + ]""" + var fa := GdFunctionArgument.new("test_parameters", TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).contains_exactly([ + """["test_a", null, "LOG", {}]""", + """["test_b", Node2D, null, {Node2D: "ER,ROR"}]""", + """["test_c", Node2D, "LOG", {Node2D: "LOG 1"}]""" + ] + ) + + +func test_parse_argument_as_array_ends_with_additional_comma() -> void: + var test_parameters := """ + [ + [true, 'bool'], + [42, 'int'], + ['foo', 'String'], + ]""" + var fa := GdFunctionArgument.new("test_parameters", TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).contains_exactly([ + """[true, 'bool']""", + """[42, 'int']""", + """['foo', 'String']""" + ] + ) + + +func test__parse_argument_as_reference() -> void: + var test_parameters := "_test_args()" + + var fa := GdFunctionArgument.new("test_parameters", TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).is_empty() + + +func test_parse_parameter_set_with_const_data_in_array() -> void: + var test_parameters := "[_data1, _data2]" + + var fa := GdFunctionArgument.new("test_parameters", TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).contains_exactly(["_data1", "_data2"]) diff --git a/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd.uid b/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd.uid new file mode 100644 index 0000000..f35eaf6 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd.uid @@ -0,0 +1 @@ +uid://bc8t2q5rc7nbw diff --git a/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd b/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd new file mode 100644 index 0000000..6bc4155 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd @@ -0,0 +1,230 @@ +# GdUnit generated TestSuite +class_name GdFunctionDescriptorTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd' + +const RETURN_TYPE_VARIANTS_SOURCE := """ + enum MyEnum { A, B } + + @warning_ignore("untyped_declaration") + func inferred_void_pass(): + pass + + func explicit_void_pass() -> void: + pass + + @warning_ignore("untyped_declaration") + func inferred_void_return(): + return + + func explicit_void_return() -> void: + return + + @warning_ignore("untyped_declaration") + func inferred_int(): + return 42 + + func explicit_int() -> int: + return 42 + + @warning_ignore("untyped_declaration") + func inferred_bool(): + return true + + func explicit_bool() -> bool: + return true + + @warning_ignore("untyped_declaration") + func inferred_double(): + return 4.7 + + func explicit_double() -> float: + return 4.7 + + @warning_ignore("untyped_declaration") + func inferred_string(): + return "abc" + + func explicit_string() -> String: + return "abc" + + @warning_ignore("untyped_declaration") + func inferred_object(): + return Object.new() + + func explicit_object() -> Object: + return Object.new() + + @warning_ignore("untyped_declaration") + func inferred_enum(): + return MyEnum.A + + func explicit_enum() -> MyEnum: + return MyEnum.A + """ + +var _return_type_variants_script: GDScript + +func before() -> void: + _return_type_variants_script = GdScriptTestHelper.build_tmp_script(RETURN_TYPE_VARIANTS_SOURCE) + + +func test_extract_from_func_without_return_type() -> void: + # void add_sibling(sibling: Node, force_readable_name: bool = false) + var method_descriptor := GdScriptTestHelper.get_class_method_descriptor("Node", "add_sibling") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("add_sibling") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_VOID) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("sibling", GdObjects.TYPE_NODE), + GdFunctionArgument.new("force_readable_name", TYPE_BOOL, false) + ]) + + +func test_extract_from_func_with_return_type() -> void: + # Node find_child(pattern: String, recursive: bool = true, owned: bool = true) const + var method_descriptor := GdScriptTestHelper.get_class_method_descriptor("Node", "find_child") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("find_child") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(TYPE_OBJECT) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("pattern", TYPE_STRING), + GdFunctionArgument.new("recursive", TYPE_BOOL, true), + GdFunctionArgument.new("owned", TYPE_BOOL, true), + ]) + + +func test_extract_from_func_with_vararg() -> void: + # Error emit_signal(signal: StringName, ...) vararg + var method_descriptor := GdScriptTestHelper.get_class_method_descriptor("Node", "emit_signal") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("emit_signal") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_true() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_ENUM) + assert_array(fd.args()).contains_exactly([GdFunctionArgument.new("signal", TYPE_STRING_NAME)]) + assert_array(fd.varargs()).contains_exactly([ + GdFunctionArgument.new("varargs", GdObjects.TYPE_VARARG, '') + ]) + + +func test_extract_from_descriptor_is_virtual_func() -> void: + var method_descriptor := GdScriptTestHelper.get_class_method_descriptor("Node", "_enter_tree") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("_enter_tree") + assert_bool(fd.is_virtual()).is_true() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_VOID) + assert_array(fd.args()).is_empty() + + +func test_extract_from_descriptor_is_virtual_func_full_check() -> void: + var methods := ClassDB.class_get_method_list("Node") + var expected_virtual_functions := [ + "_process", + "_physics_process", + "_enter_tree", + "_exit_tree", + "_ready", + "_get_configuration_warnings", + "_get_accessibility_configuration_warnings", + "_input", + "_shortcut_input", + "_unhandled_input", + "_unhandled_key_input", + "_get_focused_accessibility_element", + "_init", + "_to_string", + "_notification", + "_set", + "_get", + "_get_property_list", + "_validate_property", + "_property_can_revert", + "_property_get_revert", + "_iter_init", + "_iter_next", + "_iter_get" + ] + + var _count := 0 + for method_descriptor in methods: + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + + if fd.is_virtual(): + _count += 1 + assert_array(expected_virtual_functions).contains([fd.name()]) + assert_int(_count).is_equal(expected_virtual_functions.size()) + + +func test_extract_from_func_with_return_type_variant() -> void: + var method_descriptor := GdScriptTestHelper.get_class_method_descriptor("Node", "get") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("get") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_VARIANT) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("property", TYPE_STRING_NAME), + ]) + + +#region extract_from return types +func test_extract_from_return_types(func_name: String, expected_type: int, _test_parameters := [ + # Explicit return expression: always the correc type + ["explicit_bool", TYPE_BOOL], + ["explicit_double", TYPE_FLOAT], + ["explicit_enum", GdObjects.TYPE_ENUM], + ["explicit_int", TYPE_INT], + ["explicit_object", TYPE_OBJECT], + ["explicit_string", TYPE_STRING], + ["explicit_void_pass", GdObjects.TYPE_VOID], + ["explicit_void_return", GdObjects.TYPE_VOID], + # Inferred return expression: always TYPE_VARIANT + ["inferred_bool", GdObjects.TYPE_VARIANT], + ["inferred_double", GdObjects.TYPE_VARIANT], + ["inferred_enum", GdObjects.TYPE_VARIANT], + ["inferred_int", GdObjects.TYPE_VARIANT], + ["inferred_object", GdObjects.TYPE_VARIANT], + ["inferred_string", GdObjects.TYPE_VARIANT], + ["inferred_void_return", GdObjects.TYPE_VARIANT], +]) -> void: + var method_descriptor := GdScriptTestHelper.get_script_method_descriptor(_return_type_variants_script, func_name) + var fd := GdFunctionDescriptor.extract_from(method_descriptor, false) + assert_int(fd.return_type()).is_equal(expected_type) + + +# Since Godot 4.7 (https://github.com/godotengine/godot/pull/118032), untyped functions +# correctly report TYPE_VARIANT via PROPERTY_USAGE_NIL_IS_VARIANT. On earlier versions, +# both untyped and explicit-void pass-body functions report usage=6 and are indistinguishable. +func test_extract_from_inferred_void_pass() -> void: + var method_descriptor := GdScriptTestHelper.get_script_method_descriptor(_return_type_variants_script, "inferred_void_pass") + var fd := GdFunctionDescriptor.extract_from(method_descriptor, false) + var version := Engine.get_version_info() + var expected_type: int = GdObjects.TYPE_VARIANT if version["minor"] >= 7 else GdObjects.TYPE_VOID + assert_int(fd.return_type()).is_equal(expected_type) +#endregion + + +@warning_ignore("unused_parameter") +func example_signature(info: String, expected: int, test_parameters := [ + ["aaa", 10], + ["bbb", 11], +]) -> void: + pass diff --git a/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd.uid b/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd.uid new file mode 100644 index 0000000..1662330 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd.uid @@ -0,0 +1 @@ +uid://cr2brceqbia0s diff --git a/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd b/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd new file mode 100644 index 0000000..78aecf2 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd @@ -0,0 +1,742 @@ +extends GdUnitTestSuite + +var _parser: GdScriptParser + +const TYPE_VOID = GdObjects.TYPE_VOID +const TYPE_VARIANT = GdObjects.TYPE_VARIANT +const TYPE_VARARG = GdObjects.TYPE_VARARG +const TYPE_FUNC = GdObjects.TYPE_FUNC +const TYPE_FUZZER = GdObjects.TYPE_FUZZER +const TYPE_ENUM = GdObjects.TYPE_ENUM + + +func before() -> void: + _parser = GdScriptParser.new() + + +func after() -> void: + clean_temp_dir() + + +func test_parse_function_arguments() -> void: + assert_array(_parser._parse_function_arguments("func foo():")) \ + .has_size(0) + + assert_array(_parser._parse_function_arguments("func foo() -> String:\n")) \ + .has_size(0) + + assert_array(_parser._parse_function_arguments("func foo(arg1, arg2, name):")) \ + .has_size(3)\ + .contains([ + {"name" : "arg1", "type" : TYPE_VARIANT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "name", "type" : TYPE_VARIANT, "value" : GdFunctionArgument.UNDEFINED}, + ]) + + assert_array(_parser._parse_function_arguments('func foo(arg1 :int, arg2 :bool, name :String = "abc"):')) \ + .has_size(3)\ + .contains([ + {"name" : "arg1", "type" : TYPE_INT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_BOOL, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "name", "type" : TYPE_STRING, "value" : '"abc"'}, + ]) + + assert_array(_parser._parse_function_arguments('func bar(arg1 :int, arg2 :int = 23, name :String = "test") -> String:')) \ + .has_size(3)\ + .contains([ + {"name" : "arg1", "type" : TYPE_INT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_INT, "value" : "23"}, + {"name" : "name", "type" : TYPE_STRING, "value" : '"test"'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1, arg2=value(1,2,3), name:=foo()):")) \ + .has_size(3)\ + .contains([ + {"name" : "arg1", "type" : TYPE_VARIANT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : "value(1,2,3)"}, + {"name" : "name", "type" : TYPE_VARIANT, "value" : "foo()"}, + ]) + + # enum as prefix in value name + assert_array(_parser._parse_function_arguments("func get_value( type := ENUM_A) -> int:"))\ + .has_size(1)\ + .contains([ + {"name" : "type", "type" : TYPE_VARIANT, "value" : "ENUM_A"}, + ]) + + assert_array(_parser._parse_function_arguments("func create_timer(timeout :float) -> Timer:")) \ + .has_size(1)\ + .contains([ + {"name" : "timeout", "type" : TYPE_FLOAT, "value" : GdFunctionArgument.UNDEFINED}, + ]) + + # array argument + assert_array(_parser._parse_function_arguments("func foo(a :int, b :int, parameters = [[1, 2], [3, 4], [5, 6]]):")) \ + .has_size(3)\ + .contains([ + {"name" : "a", "type" : TYPE_INT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "b", "type" : TYPE_INT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "parameters", "type" : TYPE_VARIANT, "value" : "[[1, 2], [3, 4], [5, 6]]"}, + ]) + + assert_array(_parser._parse_function_arguments("func test_values(a:Vector2, b:Vector2, expected:Vector2, test_parameters:=[[Vector2.ONE,Vector2.ONE,Vector2(1,1)]]):"))\ + .has_size(4)\ + .contains([ + {"name" : "a", "type" : TYPE_VECTOR2, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "b", "type" : TYPE_VECTOR2, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "expected", "type" : TYPE_VECTOR2, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "test_parameters", "type" : TYPE_VARIANT, "value" : "[[Vector2.ONE,Vector2.ONE,Vector2(1,1)]]"}, + ]) + + +func test_parse_arguments_with_super_constructor() -> void: + assert_array(_parser._parse_function_arguments('func foo().foo("abc"):')).is_empty() + assert_array(_parser._parse_function_arguments('func foo(arg1 = "arg").foo("abc", arg1):'))\ + .has_size(1)\ + .contains([ + {"name" : "arg1", "type" : TYPE_VARIANT, "value" : '"arg"'}, + ]) + + +func test_parse_arguments_default_build_in_type_string() -> void: + assert_array(_parser._parse_function_arguments('func foo(arg1 :String, arg2="default"):')) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : '"default"'}, + ]) + + assert_array(_parser._parse_function_arguments('func foo(arg1 :String, arg2 :="default"):')) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : '"default"'}, + ]) + + assert_array(_parser._parse_function_arguments('func foo(arg1 :String, arg2 :String ="default"):')) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_STRING, "value" : '"default"'}, + ]) + + +func test_parse_arguments_default_build_in_type_boolean() -> void: + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2=false):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'false'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :=false):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'false'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :bool=false):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_BOOL, "value" : 'false'}, + ]) + + +func test_parse_arguments_default_build_in_type_float() -> void: + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2=3.14):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : '3.14'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :=3.14):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : '3.14'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :float=3.14):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_FLOAT, "value" : '3.14'}, + ]) + + +func test_parse_arguments_default_build_in_type_array() -> void: + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :Array=[]):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_ARRAY, "value" : '[]'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :Array=Array()):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_ARRAY, "value" : 'Array()'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :Array=[1, 2, 3]):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_ARRAY, "value" : '[1, 2, 3]'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :=[1, 2, 3]):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : '[1, 2, 3]'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2=[]):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : '[]'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :Array=[1, 2, 3], arg3 := false):")) \ + .has_size(3)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_ARRAY, "value" : '[1, 2, 3]'}, + {"name" : "arg3", "type" : TYPE_VARIANT, "value" : 'false'}, + ]) + + +func test_parse_arguments_default_build_in_type_color() -> void: + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2=Color.RED):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'Color.RED'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :=Color.RED):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'Color.RED'}, + ]) + + assert_array(_parser._parse_function_arguments("func foo(arg1 :String, arg2 :Color=Color.RED):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_COLOR, "value" : 'Color.RED'}, + ]) + + +func test_parse_arguments_default_build_in_type_vector() -> void: + assert_array(_parser._parse_function_arguments("func bar(arg1 :String, arg2 =Vector3.FORWARD):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'Vector3.FORWARD'}, + ]) + + assert_array(_parser._parse_function_arguments("func bar(arg1 :String, arg2 :=Vector3.FORWARD):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'Vector3.FORWARD'}, + ]) + + assert_array(_parser._parse_function_arguments("func bar(arg1 :String, arg2 :Vector3=Vector3.FORWARD):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VECTOR3, "value" : 'Vector3.FORWARD'}, + ]) + + +func test_parse_arguments_default_build_in_type_AABB() -> void: + assert_array(_parser._parse_function_arguments("func bar(arg1 :String, arg2 := AABB()):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'AABB()'}, + ]) + + assert_array(_parser._parse_function_arguments("func bar(arg1 :String, arg2 :AABB=AABB()):")) \ + .has_size(2)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_AABB, "value" : 'AABB()'}, + ]) + + +func test_parse_arguments_default_build_in_types() -> void: + assert_array(_parser._parse_function_arguments("func bar(arg1 :String, arg2 := Vector3.FORWARD, aabb := AABB()):")) \ + .has_size(3)\ + .contains([ + {"name" : "arg1", "type" : TYPE_STRING, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_VARIANT, "value" : 'Vector3.FORWARD'}, + {"name" : "aabb", "type" : TYPE_VARIANT, "value" : 'AABB()'}, + ]) + + +func test_parse_arguments_fuzzers() -> void: + assert_array(_parser._parse_function_arguments("func test_foo(fuzzer_a = fuzz_a(), fuzzer_b := fuzz_b(), fuzzer_c :Fuzzer = fuzz_c(), fuzzer_iterations = 234, fuzzer_seed = 100):")) \ + .has_size(5)\ + .contains([ + {"name" : "fuzzer_a", "type" : TYPE_FUZZER, "value" : 'fuzz_a()'}, + {"name" : "fuzzer_b", "type" : TYPE_FUZZER, "value" : 'fuzz_b()'}, + {"name" : "fuzzer_c", "type" : TYPE_FUZZER, "value" : 'fuzz_c()'}, + {"name" : "fuzzer_iterations", "type" : TYPE_VARIANT, "value" : '234'}, + {"name" : "fuzzer_seed", "type" : TYPE_VARIANT, "value" : '100'}, + ]) + + +func test_parse_arguments_typed_dict() -> void: + assert_array(_parser._parse_function_arguments('func generate(arg1: Dictionary[String,Variant], arg2 :Dictionary = {"a":1, "b":2}, arg3 := {}) -> void:'))\ + .has_size(3)\ + .contains([ + {"name" : "arg1", "type" : TYPE_DICTIONARY, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_DICTIONARY, "value" : '{"a":1, "b":2}'}, + {"name" : "arg3", "type" : TYPE_VARIANT, "value" : '{}'}, + ]) + + +func test_parse_arguments_variant() -> void: + # remove this line and complete your test + var args := _parser._parse_function_arguments('func generate( + ucids: Array[int], + position: Vector2i, + size: Vector2i, + style: int, + text: Variant = "", + button_name := "", + type_in := 0, + caption := "", + show_everywhere := false') + + assert_array(args)\ + .has_size(9)\ + .contains([ + {"name" : "ucids", "type" : TYPE_ARRAY, "value" : GdFunctionArgument.UNDEFINED, }, + {"name" : "position", "type" : TYPE_VECTOR2I, "value" : GdFunctionArgument.UNDEFINED, }, + {"name" : "size", "type" : TYPE_VECTOR2I, "value" : GdFunctionArgument.UNDEFINED }, + {"name" : "style", "type" : TYPE_INT, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "text", "type" : TYPE_VARIANT, "value" : '""'}, + {"name" : "button_name", "type" : TYPE_VARIANT, "value" : '""'}, + {"name" : "type_in", "type" : TYPE_VARIANT, "value" : '0'}, + {"name" : "caption", "type" : TYPE_VARIANT, "value" : '""'}, + {"name" : "show_everywhere", "type" : TYPE_VARIANT, "value" : 'false'}, + ]) + + +func test_parse_arguments_variadic() -> void: + var args := _parser._parse_function_arguments('func call(...args: Array) -> Variant:') + assert_array(args)\ + .has_size(1)\ + .contains([ + {"name" : "args", "type" : TYPE_VARARG, "value" : GdFunctionArgument.UNDEFINED} + ]) + + var script := GDScript.new() + script.source_code = """ + @warning_ignore("unused_parameter") + func custom(name: String, ...varargs: Array) -> int: + return 0 + """.dedent() + script.reload() + + # Try to enrich function arguments should not affect the argumens on variadic argument + var fds := _parser.get_function_descriptors(script, ["custom"]) + _parser._enrich_function_descriptor(script, fds) + + +func test_parse_arguments_typed_array() -> void: + # remove this line and complete your test + assert_array(_parser._parse_function_arguments("func generate(arg1: Array, arg2: Array = [1,2,3], arg3: Array[int] = [4,5,6], arg4 := []) -> void:"))\ + .has_size(4)\ + .contains([ + {"name" : "arg1", "type" : TYPE_ARRAY, "value" : GdFunctionArgument.UNDEFINED}, + {"name" : "arg2", "type" : TYPE_ARRAY, "value" : '[1,2,3]'}, + {"name" : "arg3", "type" : TYPE_ARRAY, "value" : '[4,5,6]'}, + {"name" : "arg4", "type" : TYPE_VARIANT, "value" : '[]'}, + ]) + + +class TestObject: + var x: int + + +func test_parse_func_name() -> void: + assert_str(_parser.parse_func_name("func foo():")).is_equal("foo") + assert_str(_parser.parse_func_name("static func foo():")).is_equal("foo") + assert_str(_parser.parse_func_name("func a() -> String:")).is_equal("a") + # function name contains tokens e.g func or class + assert_str(_parser.parse_func_name("func foo_func_class():")).is_equal("foo_func_class") + # unicode characters in the name + assert_str(_parser.parse_func_name("func test_ๆ—ฅๆœฌ่ชž() -> void:")).is_equal("test_ๆ—ฅๆœฌ่ชž") + # should fail + assert_str(_parser.parse_func_name("#func foo():")).is_empty() + assert_str(_parser.parse_func_name("var x")).is_empty() + +func test_load_source_code_inner_class_AtmosphereData() -> void: + var base_class := AdvancedTestClass.new() + @warning_ignore("unsafe_cast") + var rows := _parser._load_inner_class(base_class.get_script() as GDScript, "AtmosphereData") + var file_content := resource_as_string("res://addons/gdUnit4/test/core/resources/AtmosphereData.txt") + assert_that(rows).is_equal(file_content) + + +func test_load_source_code_inner_class_SoundData() -> void: + var base_class := AdvancedTestClass.new() + @warning_ignore("unsafe_cast") + var rows := _parser._load_inner_class(base_class.get_script() as GDScript, "SoundData") + var file_content := resource_as_string("res://addons/gdUnit4/test/core/resources/SoundData.txt") + assert_that(rows).is_equal(file_content) + + +func test_load_source_code_inner_class_Area4D() -> void: + var base_class := AdvancedTestClass.new() + @warning_ignore("unsafe_cast") + var rows := _parser._load_inner_class(base_class.get_script() as GDScript, "Area4D") + var file_content := resource_as_string("res://addons/gdUnit4/test/core/resources/Area4D.txt") + assert_that(rows).is_equal(file_content) + + +func test_extract_function_signature() -> void: + var script :GDScript = load("res://addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd") + var rows := script.source_code.split("\n") + + assert_that(_parser.extract_func_signature(rows, 12))\ + .is_equal(""" + func a1(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false + ) -> void:""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 19))\ + .is_equal(""" + func a2(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false + ) -> void:""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 26))\ + .is_equal(""" + func a3(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false + ) -> void:""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 33))\ + .is_equal(""" + func a4(set_name:String, + path:String="", + load_on_init:bool=false, + set_auto_save:bool=false, + set_network_sync:bool=false + ) -> void:""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 43))\ + .is_equal(""" + func a5( + value : Array, + expected : String, + test_parameters : Array = [ + [ ["a"], "a" ], + [ ["a", "very", "long", "argument"], "a very long argument" ], + ] + ) -> void:""".dedent().trim_prefix("\n")) + + +func test_strip_leading_spaces() -> void: + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces("")).is_empty() + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces(" ")).is_empty() + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces(" ")).is_empty() + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces(" ")).is_equal(" ") + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces("var x=")).is_equal("var x=") + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces("class foo")).is_equal("class foo") + + +func test_parse_func_description() -> void: + var script := GdScriptTestHelper.build_tmp_script(""" + + @warning_ignore("unused_parameter") + func foo0(arg1: int, arg2: bool=false) -> String: + return "" + + @warning_ignore("unused_parameter") + static func foo1(arg1: int, arg2: bool=false) -> String: + return "" + + @warning_ignore("untyped_declaration", "unused_parameter") + static func foo2(arg1: int, arg2: bool=true): + return + """) + var fds := _parser.get_function_descriptors(script) + + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor.create("foo0", script.resource_path, 4, TYPE_STRING, [ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_BOOL, false) + ])) + + # static function + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor.create_static("foo1", script.resource_path, 8, TYPE_STRING, [ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_BOOL, false) + ])) + + # static function without return type + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor.create_static("foo2", script.resource_path, 12, GdObjects.TYPE_VARIANT, [ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_BOOL, true) + ])) + + +func test_get_function_descriptors_return_type_enum() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd") + var fds := _parser.get_function_descriptors(script, ["get_enum"]) + + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("get_enum", script.resource_path, 15, GdObjects.TYPE_ENUM) + .with_return_class("ClassWithEnumReturnTypes.TEST_ENUM") + ) + + +func test_parse_func_description_return_type_internal_class_enum() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd") + var fds := _parser.get_function_descriptors(script, ["get_inner_class_enum"]) + + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("get_inner_class_enum", script.resource_path, 25, GdObjects.TYPE_ENUM) + .with_return_class("ClassWithEnumReturnTypes.InnerClass.TEST_ENUM") + ) + + +func test_parse_func_description_return_type_external_class_enum() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd") + var fds := _parser.get_function_descriptors(script, ["get_external_class_enum"]) + + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("get_external_class_enum", script.resource_path, 20, GdObjects.TYPE_ENUM) + .with_return_class("CustomEnums.TEST_ENUM") + ) + + +func test_parse_class_inherits() -> void: + var clazz_path := GdObjects.extract_class_path(CustomClassExtendsCustomClass) + var clazz_name := GdObjects.extract_class_name_from_class_path(clazz_path) + var result := _parser.parse(clazz_name, clazz_path) + assert_result(result).is_success() + + # verify class extraction + var clazz_desccriptor :GdClassDescriptor = result.value() + assert_object(clazz_desccriptor).is_not_null() + assert_str(clazz_desccriptor.name()).is_equal("CustomClassExtendsCustomClass") + assert_bool(clazz_desccriptor.is_inner_class()).is_false() + assert_array(clazz_desccriptor.functions())\ + .contains_exactly([ + GdFunctionDescriptor.create("foo2", "res://addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd", 6, GdObjects.TYPE_VARIANT), + GdFunctionDescriptor.create("bar2", "res://addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd", 9, TYPE_STRING), + GdFunctionDescriptor.create("foo", "res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd", 4, TYPE_STRING), + GdFunctionDescriptor.create("foo_void", "res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd", 10, GdObjects.TYPE_VOID), + GdFunctionDescriptor.create("bar", "res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd", 13, TYPE_STRING, [ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_INT, 23), + GdFunctionArgument.new("name", TYPE_STRING, "test"), + ]), + # see https://github.com/godotengine/godot/pull/118032 + GdFunctionDescriptor.create("foo5", "res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd", 17, + GdObjects.TYPE_VARIANT if Engine.get_version_info()["minor"] >= 7 else GdObjects.TYPE_VOID), + ]) + + +func test_get_class_name_pascal_case() -> void: + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd") as GDScript))\ + .is_equal("PascalCaseWithClassName") + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd") as GDScript))\ + .is_equal("PascalCaseWithoutClassName") + + +func test_get_class_name_snake_case() -> void: + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd") as GDScript))\ + .is_equal("SnakeCaseWithClassName") + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd") as GDScript))\ + .is_equal("SnakeCaseWithoutClassName") + + +func test_get_class_with_extends_in_same_line() -> void: + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/extends_on_same_line.gd") as GDScript))\ + .is_equal("ClassNameExtendsInSameLine") + + +func test_is_func_end() -> void: + assert_bool(_parser.is_func_end("")).is_false() + assert_bool(_parser.is_func_end("func test_a():")).is_true() + assert_bool(_parser.is_func_end("func test_a() -> void:")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1) :")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1 ): ")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1 ): ")).is_true() + assert_bool(_parser.is_func_end(" ):")).is_true() + assert_bool(_parser.is_func_end(" ):")).is_true() + assert_bool(_parser.is_func_end(" -> void:")).is_true() + assert_bool(_parser.is_func_end(" ) -> void :")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1, arg2 = {1:2} ):")).is_true() + + +func test_extract_func_signature_multiline() -> void: + var source_code := """ + + func test_parameterized(a: int, b :int, c :int, expected :int, test_parameters = [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]): + + assert_that(a+b+c).is_equal(expected) + """.dedent().split("\n") + + var fs := _parser.extract_func_signature(source_code, 0) + + assert_that(fs).is_equal(""" + func test_parameterized(a: int, b :int, c :int, expected :int, test_parameters = [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]):""" + .dedent() + .trim_prefix("\n") + ) + + +func test_parse_func_description_paramized_test() -> void: + var script := GdScriptTestHelper.build_tmp_script(""" + @warning_ignore("unused_parameter") + func test_parameterized(a: int, b: int, c: int, expected: int, test_parameters: Array = [[1,2,3,6],[3,4,5,11],[6,7,8,21]]) -> Variant: + return null + """) + var fds := GdScriptParser.new().get_function_descriptors(script, ["test_parameterized"]) + + assert_that(fds[0]).is_equal(GdFunctionDescriptor.create("test_parameterized", script.resource_path, 3, GdObjects.TYPE_VARIANT, [ + GdFunctionArgument.new("a", TYPE_INT), + GdFunctionArgument.new("b", TYPE_INT), + GdFunctionArgument.new("c", TYPE_INT), + GdFunctionArgument.new("expected", TYPE_INT), + GdFunctionArgument.new("test_parameters", TYPE_ARRAY, "[[1,2,3,6],[3,4,5,11],[6,7,8,21]]"), + ])) + + +func test_parse_func_description_paramized_test_with_comments() -> void: + var source_code := """ + func test_parameterized(a: int, b :int, c :int, expected :int, test_parameters = [ + # before data set + [1, 2, 3, 6], # after data set + # between data sets + [3, 4, 5, 11], + [6, 7, 'string #ABCD', 21], # dataset with [comment] singn + [6, 7, "string #ABCD", 21] # dataset with "#comment" singn + #eof + ]): + pass + """.dedent().split("\n") + + var fs := _parser.extract_func_signature(source_code, 0) + + assert_that(fs).is_equal(""" + func test_parameterized(a: int, b :int, c :int, expected :int, test_parameters = [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 'string #ABCD', 21], + [6, 7, "string #ABCD", 21] + ]):""" + .dedent() + .trim_prefix("\n") + ) + + +func test_parse_func_descriptor_with_fuzzers() -> void: + # using a mixure of typed and untyped default values + var script := GdScriptTestHelper.build_tmp_script(""" + func fuzz_a() -> Fuzzer: + return Fuzzers.rangef(0, 10) + + func fuzz_b() -> Fuzzer: + return Fuzzers.rangef(0, 10) + + func fuzz_c() -> Fuzzer: + return Fuzzers.rangef(0, 10) + + @warning_ignore("untyped_declaration", "unused_parameter", "inferred_declaration") + func test_foo(fuzzer_a = fuzz_a(), fuzzer_b := fuzz_b(), + fuzzer_c :Fuzzer = fuzz_c(), + fuzzer = Fuzzers.rangei(-23, 22), + fuzzer_iterations = 234, + fuzzer_seed := 100): + pass + """) + var fds := _parser.get_function_descriptors(script, ["test_foo"]) + # see https://github.com/godotengine/godot/pull/118032 + var untyped_return_type: int = GdObjects.TYPE_VARIANT if Engine.get_version_info()["minor"] >= 7 else GdObjects.TYPE_VOID + + assert_that(fds[0]).is_equal(GdFunctionDescriptor.create("test_foo", script.resource_path, 12, untyped_return_type, [ + # all fuzzer must by type TYPE_FUZZER + GdFunctionArgument.new("fuzzer_a", GdObjects.TYPE_FUZZER, "fuzz_a()"), + GdFunctionArgument.new("fuzzer_b", GdObjects.TYPE_FUZZER, "fuzz_b()"), + GdFunctionArgument.new("fuzzer_c", GdObjects.TYPE_FUZZER, "fuzz_c()"), + GdFunctionArgument.new("fuzzer", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(-23, 22)"), + # untyped arg is TYPE_VARIANT + GdFunctionArgument.new("fuzzer_iterations", GdObjects.TYPE_VARIANT, 234), + # typed is TYPE_INT + GdFunctionArgument.new("fuzzer_seed", TYPE_INT, 100) + ])) + + +func test_is_func_coroutine() -> void: + var script := """ + extends RefCounted: + func normal_function() -> void: + print("normal") + + + func await_function() -> void: + print(await _await_function()) + + + func _await_function() -> String: + await get_tree().process_frame + return "test" + + + func check_is_waiting() -> String: + return is_await() + + + func print_message() -> String: + return "do await for timeout" + """ + + var rows := script.split("\n") + assert_bool(_parser.is_func_coroutine(rows, 2)).is_false() + assert_bool(_parser.is_func_coroutine(rows, 6)).is_true() + assert_bool(_parser.is_func_coroutine(rows, 10)).is_true() + assert_bool(_parser.is_func_coroutine(rows, 15)).is_false() + assert_bool(_parser.is_func_coroutine(rows, 19)).is_false() + + +func test_using_class_in_variable_name() -> void: + var script := """ + extends RefCounted: + func foo() -> void: + var class1.a = "a" + var class2 = "abc" + + + func bar() -> void: + print("bar") + + """ + + var rows := script.split("\n") + assert_bool(_parser.is_func_coroutine(rows, 0)).is_false() + assert_bool(_parser.is_func_coroutine(rows, 4)).is_false() diff --git a/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd.uid b/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd.uid new file mode 100644 index 0000000..55b203c --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd.uid @@ -0,0 +1 @@ +uid://diy0idlgibce4 diff --git a/addons/gdUnit4/test/core/parse/GdScriptParsingFunctionDescriptors.gd b/addons/gdUnit4/test/core/parse/GdScriptParsingFunctionDescriptors.gd new file mode 100644 index 0000000..e4c6eb3 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdScriptParsingFunctionDescriptors.gd @@ -0,0 +1,506 @@ +extends GdUnitTestSuite + +var _parser: GdScriptParser + + +func before() -> void: + _parser = GdScriptParser.new() + + +func after() -> void: + clean_temp_dir() + + +func test_default_args_dictionary() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithDictionaryDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_dictionary_case1", script.resource_path, 5, TYPE_ARRAY, [ + GdFunctionArgument.new("dict", TYPE_DICTIONARY, {}), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_dictionary_case2", script.resource_path, 9, TYPE_ARRAY, [ + GdFunctionArgument.new("dict", TYPE_DICTIONARY, {}), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_dictionary_case3", script.resource_path, 13, TYPE_ARRAY, [ + GdFunctionArgument.new("dict", TYPE_DICTIONARY, {}), + ]) + ) + assert_that(fds[3])\ + .is_equal(GdFunctionDescriptor + .create("on_dictionary_case4", script.resource_path, 17, TYPE_ARRAY, [ + GdFunctionArgument.new("dict", TYPE_DICTIONARY, {"a":"value_a", "b":"value_b"}), + ]) + ) + assert_that(fds[4])\ + .is_equal(GdFunctionDescriptor + .create("on_dictionary_case5", script.resource_path, 21, TYPE_ARRAY, [ + GdFunctionArgument.new("dict", TYPE_DICTIONARY, {"a":"value_a", "b":"value_b"}), + ]) + ) + assert_that(fds[5])\ + .is_equal(GdFunctionDescriptor + .create("on_dictionary_case6", script.resource_path, 27, TYPE_ARRAY, [ + GdFunctionArgument.new("dict", TYPE_DICTIONARY, {"a":"value_a", "b":"value_b"}), + ]) + ) + + +func test_default_args_array() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithArrayDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_array_case1", script.resource_path, 5, TYPE_ARRAY, [ + GdFunctionArgument.new("values", TYPE_ARRAY, []), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_array_case2", script.resource_path, 9, TYPE_ARRAY, [ + GdFunctionArgument.new("values", TYPE_ARRAY, []), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_array_case3", script.resource_path, 13, TYPE_ARRAY, [ + GdFunctionArgument.new("values", TYPE_ARRAY, []), + ]) + ) + assert_that(fds[3])\ + .is_equal(GdFunctionDescriptor + .create("on_array_case4", script.resource_path, 17, TYPE_ARRAY, [ + GdFunctionArgument.new("values", TYPE_ARRAY, [1, "3", [], {}]), + ]) + ) + assert_that(fds[4])\ + .is_equal(GdFunctionDescriptor + .create("on_array_case5", script.resource_path, 21, TYPE_ARRAY, [ + GdFunctionArgument.new("values", TYPE_ARRAY, [1, "3", [], {}]), + ]) + ) + assert_that(fds[5])\ + .is_equal(GdFunctionDescriptor + .create("on_array_case6", script.resource_path, 29, TYPE_ARRAY, [ + GdFunctionArgument.new("values", TYPE_ARRAY, [1, "3", [], {}]), + ]) + ) + + +func test_default_args_callable() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithCallableDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_callable_case1", script.resource_path, 5, TYPE_CALLABLE, [ + GdFunctionArgument.new("cb", TYPE_CALLABLE), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_callable_case2", script.resource_path, 9, TYPE_CALLABLE, [ + GdFunctionArgument.new("cb", TYPE_CALLABLE, Callable()), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_callable_case3", script.resource_path, 13, TYPE_CALLABLE, [ + GdFunctionArgument.new("cb", TYPE_CALLABLE, Callable()), + ]) + ) + assert_that(fds[3])\ + .is_equal(GdFunctionDescriptor + .create("on_callable_case4", script.resource_path, 17, TYPE_CALLABLE, [ + GdFunctionArgument.new("cb", TYPE_CALLABLE, Callable(null, "method_foo")), + ]) + ) + + +func test_default_args_variant() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/ClassWithVariantTypeDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("add_button", script.resource_path, 4, GdObjects.TYPE_VOID, [ + GdFunctionArgument.new("ucids", TYPE_ARRAY, GdFunctionArgument.UNDEFINED, TYPE_INT), + GdFunctionArgument.new("position", TYPE_VECTOR2I), + GdFunctionArgument.new("size", TYPE_VECTOR2I), + GdFunctionArgument.new("style", TYPE_INT), + GdFunctionArgument.new("text", GdObjects.TYPE_VARIANT, ""), + GdFunctionArgument.new("button_name", TYPE_STRING, ""), + GdFunctionArgument.new("type_in", TYPE_INT, 0), + GdFunctionArgument.new("caption", TYPE_STRING, ""), + GdFunctionArgument.new("show_everywhere", TYPE_BOOL, false), + ]) + ) + + +# Basic build-in-types +func test_default_args_basic_type_int() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeIntDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_int_case1", script.resource_path, 5, TYPE_INT, [ + GdFunctionArgument.new("value", TYPE_INT), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_int_case2", script.resource_path, 9, TYPE_INT, [ + GdFunctionArgument.new("value", TYPE_INT, 42), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_int_case3", script.resource_path, 13, TYPE_INT, [ + GdFunctionArgument.new("value", TYPE_INT, 42), + ]) + ) + + +func test_default_args_basic_type_float() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeFloatDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_float_case1", script.resource_path, 5, TYPE_FLOAT, [ + GdFunctionArgument.new("value", TYPE_FLOAT), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_float_case2", script.resource_path, 9, TYPE_FLOAT, [ + GdFunctionArgument.new("value", TYPE_FLOAT, 42.1), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_float_case3", script.resource_path, 13, TYPE_FLOAT, [ + GdFunctionArgument.new("value", TYPE_FLOAT, 42.1), + ]) + ) + + +func test_default_args_basic_type_bool() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeBoolDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_bool_case1", script.resource_path, 5, TYPE_BOOL, [ + GdFunctionArgument.new("value", TYPE_BOOL), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_bool_case2", script.resource_path, 9, TYPE_BOOL, [ + GdFunctionArgument.new("value", TYPE_BOOL, true), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_bool_case3", script.resource_path, 13, TYPE_BOOL, [ + GdFunctionArgument.new("value", TYPE_BOOL, true), + ]) + ) + + +func test_default_args_basic_type_string() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_string_case1", script.resource_path, 5, TYPE_STRING, [ + GdFunctionArgument.new("value", TYPE_STRING), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_string_case2", script.resource_path, 9, TYPE_STRING, [ + GdFunctionArgument.new("value", TYPE_STRING, "foo"), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_string_case3", script.resource_path, 13, TYPE_STRING, [ + GdFunctionArgument.new("value", TYPE_STRING, "foo"), + ]) + ) + + +func test_default_args_basic_type_string_name() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringNameDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_string_name_case1", script.resource_path, 5, TYPE_STRING_NAME, [ + GdFunctionArgument.new("value", TYPE_STRING_NAME), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_string_name_case2", script.resource_path, 9, TYPE_STRING_NAME, [ + GdFunctionArgument.new("value", TYPE_STRING_NAME, &"foo"), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_string_name_case3", script.resource_path, 13, TYPE_STRING_NAME, [ + GdFunctionArgument.new("value", TYPE_STRING_NAME, &"foo"), + ]) + ) + + +func test_default_args_basic_type_node_path() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeNodePathDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_node_path_case1", script.resource_path, 5, TYPE_NODE_PATH, [ + GdFunctionArgument.new("value", TYPE_NODE_PATH), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_node_path_case2", script.resource_path, 9, TYPE_NODE_PATH, [ + GdFunctionArgument.new("value", TYPE_NODE_PATH, NodePath("foo1")), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_node_path_case3", script.resource_path, 13, TYPE_NODE_PATH, [ + GdFunctionArgument.new("value", TYPE_NODE_PATH, NodePath("foo2")), + ]) + ) + assert_that(fds[3])\ + .is_equal(GdFunctionDescriptor + .create("on_node_path_case4", script.resource_path, 17, TYPE_NODE_PATH, [ + GdFunctionArgument.new("value", TYPE_NODE_PATH, NodePath("foo3")), + ]) + ) + + +func test_default_args_basic_type_vector2() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2DefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_vector2_case1", script.resource_path, 5, TYPE_VECTOR2, [ + GdFunctionArgument.new("value", TYPE_VECTOR2), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_vector2_case2", script.resource_path, 9, TYPE_VECTOR2, [ + GdFunctionArgument.new("value", TYPE_VECTOR2, Vector2.ONE), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_vector2_case3", script.resource_path, 13, TYPE_VECTOR2, [ + GdFunctionArgument.new("value", TYPE_VECTOR2, Vector2.ONE), + ]) + ) + + +func test_default_args_basic_type_vector2i() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2iDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_vector2i_case1", script.resource_path, 5, TYPE_VECTOR2I, [ + GdFunctionArgument.new("value", TYPE_VECTOR2I), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_vector2i_case2", script.resource_path, 9, TYPE_VECTOR2I, [ + GdFunctionArgument.new("value", TYPE_VECTOR2I, Vector2i.ONE), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_vector2i_case3", script.resource_path, 13, TYPE_VECTOR2I, [ + GdFunctionArgument.new("value", TYPE_VECTOR2I, Vector2i.ONE), + ]) + ) + + +func test_default_args_basic_type_vector3() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3DefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_vector3_case1", script.resource_path, 5, TYPE_VECTOR3, [ + GdFunctionArgument.new("value", TYPE_VECTOR3), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_vector3_case2", script.resource_path, 9, TYPE_VECTOR3, [ + GdFunctionArgument.new("value", TYPE_VECTOR3, Vector3.ONE), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_vector3_case3", script.resource_path, 13, TYPE_VECTOR3, [ + GdFunctionArgument.new("value", TYPE_VECTOR3, Vector3.ONE), + ]) + ) + + +func test_default_args_basic_type_vector3i() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3iDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_vector3i_case1", script.resource_path, 5, TYPE_VECTOR3I, [ + GdFunctionArgument.new("value", TYPE_VECTOR3I), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_vector3i_case2", script.resource_path, 9, TYPE_VECTOR3I, [ + GdFunctionArgument.new("value", TYPE_VECTOR3I, Vector3i.ONE), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_vector3i_case3", script.resource_path, 13, TYPE_VECTOR3I, [ + GdFunctionArgument.new("value", TYPE_VECTOR3I, Vector3i.ONE), + ]) + ) + + +func test_default_args_basic_type_vector4() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4DefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_vector4_case1", script.resource_path, 5, TYPE_VECTOR4, [ + GdFunctionArgument.new("value", TYPE_VECTOR4), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_vector4_case2", script.resource_path, 9, TYPE_VECTOR4, [ + GdFunctionArgument.new("value", TYPE_VECTOR4, Vector4.ONE), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_vector4_case3", script.resource_path, 13, TYPE_VECTOR4, [ + GdFunctionArgument.new("value", TYPE_VECTOR4, Vector4.ONE), + ]) + ) + + +func test_default_args_basic_type_vector4i() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4iDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_vector4i_case1", script.resource_path, 5, TYPE_VECTOR4I, [ + GdFunctionArgument.new("value", TYPE_VECTOR4I), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_vector4i_case2", script.resource_path, 9, TYPE_VECTOR4I, [ + GdFunctionArgument.new("value", TYPE_VECTOR4I, Vector4i.ONE), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_vector4i_case3", script.resource_path, 13, TYPE_VECTOR4I, [ + GdFunctionArgument.new("value", TYPE_VECTOR4I, Vector4i.ONE), + ]) + ) + + +func test_default_args_basic_type_rect2() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeRect2DefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_rect2_case1", script.resource_path, 5, TYPE_RECT2, [ + GdFunctionArgument.new("value", TYPE_RECT2), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_rect2_case2", script.resource_path, 9, TYPE_RECT2, [ + GdFunctionArgument.new("value", TYPE_RECT2, Rect2()), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_rect2_case3", script.resource_path, 13, TYPE_RECT2, [ + GdFunctionArgument.new("value", TYPE_RECT2, Rect2()), + ]) + ) + assert_that(fds[3])\ + .is_equal(GdFunctionDescriptor + .create("on_rect2_case4", script.resource_path, 17, TYPE_RECT2, [ + GdFunctionArgument.new("value", TYPE_RECT2, Rect2(Vector2.ONE, Vector2.ZERO)), + ]) + ) + assert_that(fds[4])\ + .is_equal(GdFunctionDescriptor + .create("on_rect2_case5", script.resource_path, 21, TYPE_RECT2, [ + GdFunctionArgument.new("value", TYPE_RECT2, Rect2(Vector2(0, 1), Vector2(2, 3))), + ]) + ) + + +func test_default_args_basic_type_transform2d() -> void: + var script: GDScript = load("res://addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeTransform2DDefaultArguments.gd") + + var fds := _parser.get_function_descriptors(script, []) + assert_that(fds[0])\ + .is_equal(GdFunctionDescriptor + .create("on_transform2d_case1", script.resource_path, 5, TYPE_TRANSFORM2D, [ + GdFunctionArgument.new("value", TYPE_TRANSFORM2D), + ]) + ) + assert_that(fds[1])\ + .is_equal(GdFunctionDescriptor + .create("on_transform2d_case2", script.resource_path, 9, TYPE_TRANSFORM2D, [ + GdFunctionArgument.new("value", TYPE_TRANSFORM2D, Transform2D()), + ]) + ) + assert_that(fds[2])\ + .is_equal(GdFunctionDescriptor + .create("on_transform2d_case3", script.resource_path, 13, TYPE_TRANSFORM2D, [ + GdFunctionArgument.new("value", TYPE_TRANSFORM2D, Transform2D()), + ]) + ) + assert_that(fds[3])\ + .is_equal(GdFunctionDescriptor + .create("on_transform2d_case4", script.resource_path, 17, TYPE_TRANSFORM2D, [ + GdFunctionArgument.new("value", TYPE_TRANSFORM2D, Transform2D(1.2, Vector2.ONE)), + ]) + ) diff --git a/addons/gdUnit4/test/core/parse/GdScriptParsingFunctionDescriptors.gd.uid b/addons/gdUnit4/test/core/parse/GdScriptParsingFunctionDescriptors.gd.uid new file mode 100644 index 0000000..d315d51 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdScriptParsingFunctionDescriptors.gd.uid @@ -0,0 +1 @@ +uid://magrriilknpa diff --git a/addons/gdUnit4/test/core/parse/GdScriptTestHelper.gd b/addons/gdUnit4/test/core/parse/GdScriptTestHelper.gd new file mode 100644 index 0000000..bf72bbd --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdScriptTestHelper.gd @@ -0,0 +1,37 @@ +class_name GdScriptTestHelper +extends RefCounted + + +static func build_tmp_script(source_code: String) -> GDScript: + var script := GDScript.new() + script.source_code = source_code.dedent() + script.resource_path = GdUnitFileAccess.temp_dir() + "/tmp_%d.gd" % script.get_instance_id() + var file := FileAccess.open(script.resource_path, FileAccess.WRITE) + file.store_string(script.source_code) + file.close() + + var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access") + var unused_parameter: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unused_parameter") + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0) + ProjectSettings.set_setting("debug/gdscript/warnings/unused_parameter", 0) + var error := script.reload() + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access) + ProjectSettings.set_setting("debug/gdscript/warnings/unused_parameter", unused_parameter) + if error: + push_error("Can't load temp script '%s', error: %s" % [source_code, error_string(error)]) + return null + return script + + +static func get_class_method_descriptor(clazz_name: String, method_name: String) -> Dictionary: + for descriptor: Dictionary in ClassDB.class_get_method_list(clazz_name): + if descriptor["name"] == method_name: + return descriptor + return {} + + +static func get_script_method_descriptor(script: GDScript, method_name: String) -> Dictionary: + for descriptor: Dictionary in script.get_script_method_list(): + if descriptor["name"] == method_name: + return descriptor + return {} diff --git a/addons/gdUnit4/test/core/parse/GdScriptTestHelper.gd.uid b/addons/gdUnit4/test/core/parse/GdScriptTestHelper.gd.uid new file mode 100644 index 0000000..8e52170 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdScriptTestHelper.gd.uid @@ -0,0 +1 @@ +uid://cg0dybqgixjxb diff --git a/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd b/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd new file mode 100644 index 0000000..891ec66 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd @@ -0,0 +1,72 @@ +# GdUnit generated TestSuite +class_name GdUnitExpressionsTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd' + +const TestFuzzers := preload("res://addons/gdUnit4/test/fuzzers/TestFuzzers.gd") + + +func test_create_fuzzer_argument_default() -> void: + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(GDScript.new(), "Fuzzers.rangei(-10, 22)") + assert_that(fuzzer).is_not_null() + assert_object(fuzzer).is_instanceof(Fuzzer) + assert_int(fuzzer.next_value()).is_between(-10, 22) + + +func test_create_fuzzer_argument_with_constants() -> void: + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "Fuzzers.rangei(-10, MAX_VALUE)") + assert_that(fuzzer).is_not_null() + assert_object(fuzzer).is_instanceof(Fuzzer) + assert_int(fuzzer.next_value()).is_between(-10, 22) + + +func test_create_fuzzer_argument_with_custom_function() -> void: + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "get_fuzzer()") + assert_that(fuzzer).is_not_null() + assert_object(fuzzer).is_instanceof(Fuzzer) + assert_int(fuzzer.next_value()).is_between(TestFuzzers.MIN_VALUE, TestFuzzers.MAX_VALUE) + + +func test_create_fuzzer_do_fail() -> void: + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "non_fuzzer()") + assert_that(fuzzer).is_null() + + +func test_create_nested_fuzzer_do_fail() -> void: + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "NestedFuzzer.new()") + assert_that(fuzzer).is_not_null() + assert_bool(fuzzer is Fuzzer).is_true() + assert_bool(fuzzer is TestFuzzers.NestedFuzzer).is_true() + + +func test_create_external_fuzzer() -> void: + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(GDScript.new(), "TestExternalFuzzer.new()") + assert_that(fuzzer).is_not_null() + assert_bool(fuzzer is Fuzzer).is_true() + assert_bool(fuzzer is TestExternalFuzzer).is_true() + + +func test_create_multipe_fuzzers() -> void: + var fuzzer_a := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "Fuzzers.rangei(-10, MAX_VALUE)") + var fuzzer_b := GdUnitExpressionRunner.new().to_fuzzer(GDScript.new(), "Fuzzers.rangei(10, 20)") + assert_that(fuzzer_a).is_not_null() + assert_object(fuzzer_a).is_instanceof(IntFuzzer) + var a :IntFuzzer = fuzzer_a + assert_int(a._from).is_equal(-10) + assert_int(a._to).is_equal(TestFuzzers.MAX_VALUE) + assert_that(fuzzer_b).is_not_null() + assert_object(fuzzer_b).is_instanceof(IntFuzzer) + var b :IntFuzzer = fuzzer_b + assert_int(b._from).is_equal(10) + assert_int(b._to).is_equal(20) + + +func test_create_fuzzer_with_args() -> void: + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "NestedFuzzerWithArgs.new(100, MAX_VALUE, Vector2.ONE)") + assert_that(fuzzer).is_not_null() + assert_bool(fuzzer is Fuzzer).is_true() + assert_bool(fuzzer is TestFuzzers.NestedFuzzerWithArgs).is_true() diff --git a/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd.uid b/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd.uid new file mode 100644 index 0000000..aac11de --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd.uid @@ -0,0 +1 @@ +uid://b72dhmnyfl7a6 diff --git a/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd b/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd new file mode 100644 index 0000000..dbbde5a --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd @@ -0,0 +1,217 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd' +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _test_param1 := 10 +var _test_param2 := 20 + + +func before() -> void: + _test_param1 = 11 + + +func test_before() -> void: + _test_param2 = 22 + + +func test_example_a(_a: int, _b: int, _test_parameters := [[1, 2], [3,4]]) -> void: + pass + + +func test_example_b(_a: Vector2, _b: Vector2, _test_parameters := [ + [Vector2.ZERO, Vector2.ONE], [Vector2(1.1, 3.2), Vector2.DOWN]] ) -> void: + pass + + +func test_example_c(_a: Object, _b: Object, _test_parameters := [ + [Resource.new(), Resource.new()], + [Resource.new(), null] + ] ) -> void: + pass + + +func test_resolve_parameters_static(_a: int, _b: int, _test_parameters := [ + [1, 10], + [2, 20] + ]) -> void: + pass + + +func test_resolve_parameters_at_runtime(_a: int, _b: int, _test_parameters := [ + [1, _test_param1], + [2, _test_param2], + [3, 30] + ]) -> void: + pass + + +func test_parameterized_with_comments(_a: int, _b: int, _c: String, _expected: int, _test_parameters := [ + # before data set + [1, 2, '3', 6], # after data set + # between data sets + [3, 4, '5', 11], + [6, 7, 'string #ABCD', 21], # dataset with [comment] singn + [6, 7, "string #ABCD", 21] # dataset with "comment" singn + #eof +]) -> void: + pass + + +func build_param(value: float) -> Vector3: + return Vector3(value, value, value) + + +func test_example_d(_a: Vector3, _b: Vector3, _test_parameters:=[ + [build_param(1), build_param(3)], + [Vector3.BACK, Vector3.UP] + ] ) -> void: + pass + + +class TestObj extends RefCounted: + var _value: String + + func _init(value: String) -> void: + _value = value + + func _to_string() -> String: + return _value + + +func test_example_e(_a: Object, _b: Object, _expected: String, _test_parameters:=[ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"]]) -> void: + pass + + +# verify the used 'test_parameters' is completly resolved +func test_load_parameter_sets() -> void: + assert_array(load_parameter_sets("test_example_a")) \ + .is_equal([[1, 2], [3, 4]]) + + assert_array(load_parameter_sets("test_example_b")) \ + .is_equal([[Vector2.ZERO, Vector2.ONE], [Vector2(1.1, 3.2), Vector2.DOWN]]) + + assert_array(load_parameter_sets("test_example_c")) \ + .is_equal([[Resource.new(), Resource.new()], [Resource.new(), null]]) + + assert_array(load_parameter_sets("test_example_d")) \ + .is_equal([[Vector3(1, 1, 1), Vector3(3, 3, 3)], [Vector3.BACK, Vector3.UP]]) + + assert_array(load_parameter_sets("test_example_e")) \ + .is_equal([[TestObj.new("abc"), TestObj.new("def"), "abcdef"]]) + + +func test_load_parameter_sets_at_runtime() -> void: + var params := load_parameter_sets("test_resolve_parameters_at_runtime") + assert_that(params).is_not_null() + # check the parameters resolved at runtime + assert_array(params) \ + .is_equal([ + # the value `_test_param1` is changed from 10 to 11 on `before` stage + [1, 11], + # the value `_test_param2` is changed from 20 to 2 on `test_before` stage + [2, 22], + # the value is static initial `30` + [3, 30]]) + + +func test_load_parameter_with_comments() -> void: + var params := load_parameter_sets("test_parameterized_with_comments") + assert_that(params).is_not_null() + # check the parameters resolved at runtime + assert_array(params) \ + .is_equal([ + [1, 2, '3', 6], + [3, 4, '5', 11], + [6, 7, 'string #ABCD', 21], + [6, 7, "string #ABCD", 21]]) + + +func test_validate_test_parameter_set() -> void: + var test_suite :GdUnitTestSuite = auto_free(GdUnitTestResourceLoader.load_test_suite("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource")) + + assert_is_not_skipped(test_suite, "test_no_parameters") + assert_is_not_skipped(test_suite, "test_parameterized_success", 0) + assert_is_not_skipped(test_suite, "test_parameterized_success", 1) + assert_is_not_skipped(test_suite, "test_parameterized_success", 2) + assert_is_not_skipped(test_suite, "test_parameterized_failed", 0) + assert_is_not_skipped(test_suite, "test_parameterized_failed", 1) + assert_is_not_skipped(test_suite, "test_parameterized_failed", 2) + assert_is_skipped(test_suite, "test_parameterized_to_less_args", 0).is_equal( + """ + The test data set at index (0) does not match the expected test arguments: + test function: func test...(a: int,b: int,expected: int) + test input values: [1, 2, 3, 6] + """.dedent() + ) + assert_is_skipped(test_suite, "test_parameterized_to_less_args", 1).is_equal( + """ + The test data set at index (1) does not match the expected test arguments: + test function: func test...(a: int,b: int,expected: int) + test input values: [3, 4, 5, 11] + """.dedent() + ) + assert_is_skipped(test_suite, "test_parameterized_to_many_args", 0).is_equal( + """ + The test data set at index (0) does not match the expected test arguments: + test function: func test...(a: int,b: int,c: int,d: int,expected: int) + test input values: [1, 2, 3, 6] + """.dedent() + ) + assert_is_skipped(test_suite, "test_parameterized_to_less_args", 0).is_equal( + """ + The test data set at index (0) does not match the expected test arguments: + test function: func test...(a: int,b: int,expected: int) + test input values: [1, 2, 3, 6] + """.dedent() + ) + # test_parameterized_invalid_struct + assert_is_not_skipped(test_suite, "test_parameterized_invalid_struct", 0) + assert_is_skipped(test_suite, "test_parameterized_invalid_struct", 1).is_equal( + """ + The test data set at index (1) does not match the expected test arguments: + test function: func test...(a: int,b: int,expected: int) + test input values: ["foo"] + """.dedent() + ) + assert_is_not_skipped(test_suite, "test_parameterized_invalid_struct", 2) + # test_parameterized_invalid_args + assert_is_not_skipped(test_suite, "test_parameterized_invalid_args", 0) + assert_is_skipped(test_suite, "test_parameterized_invalid_args", 1).is_equal( + """ + The test data value does not match the expected input type! + input value: '4', + expected argument: b: int + """.dedent() + ) + assert_is_not_skipped(test_suite, "test_parameterized_invalid_args", 2) + + +func assert_is_not_skipped(test_suite: GdUnitTestSuite, test_case: String, index := -1) -> void: + var test := GdUnitTools.find_test_case(test_suite, test_case, index) + if test.is_parameterized(): + # to load parameter set and force validate + test._resolve_test_parameters(index) + assert_bool(test.is_skipped()).is_false() + + +func assert_is_skipped(test_suite: GdUnitTestSuite, test_case: String, index := -1) -> GdUnitStringAssert: + var test := GdUnitTools.find_test_case(test_suite, test_case, index) + if test.is_parameterized(): + # to load parameter set and force validate + test._resolve_test_parameters(index) + assert_bool(test.is_skipped()).is_true() + return assert_str(GdUnitTools.richtext_normalize(test.skip_info())) + + +func load_parameter_sets(child_name: String) -> Array: + var script: GDScript = self.get_script() + var function_descriptors := GdScriptParser.new().get_function_descriptors(script, [child_name]) + var fd: GdFunctionDescriptor = function_descriptors.front() + var result := GdUnitTestParameterSetResolver.new(fd).load_parameter_sets(self) + if result.is_success(): + return result.value() + return [] diff --git a/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd.uid b/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd.uid new file mode 100644 index 0000000..c367ac2 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd.uid @@ -0,0 +1 @@ +uid://cm7i6ei0go7pl diff --git a/addons/gdUnit4/test/core/report/GdUnitReportTest.gd b/addons/gdUnit4/test/core/report/GdUnitReportTest.gd new file mode 100644 index 0000000..d199337 --- /dev/null +++ b/addons/gdUnit4/test/core/report/GdUnitReportTest.gd @@ -0,0 +1,94 @@ +# GdUnit generated TestSuite +class_name GdUnitReportTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/report/GdUnitReport.gd' + + +#region _to_string + +func test_to_string_failure_with_line_number() -> void: + var report := GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "test message") + assert_str(report.to_string()).is_equal("[color=green]line [/color][color=aqua]42:[/color] test message") + + +func test_to_string_failure_with_unknown_line() -> void: + var report := GdUnitReport.new().create(GdUnitReport.FAILURE, -1, "test message") + assert_str(report.to_string()).is_equal("[color=green]line [/color][color=aqua]:[/color] test message") + +#endregion + + +#region stack_trace + +func test_stack_trace_is_null_when_no_error() -> void: + var report := GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "test message") + assert_that(report.stack_trace()).is_null() + + +func test_stack_trace_returns_trace_from_error() -> void: + var trace := GdUnitStackTrace.of([ + GdUnitStackTraceElement.new("res://test.gd", 10, "func_a"), + GdUnitStackTraceElement.new("res://test.gd", 20, "func_b"), + ]) + var report := GdUnitReport.new().from_error(GdUnitReport.FAILURE, GdUnitError.new("test message", 10, trace)) + assert_that(report.stack_trace()).is_equal(trace) + +#endregion + + +#region serialize / deserialize + +func test_serialize_report_contains_all_base_fields() -> void: + var serialized := GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "test message").serialize() + assert_dict(serialized) \ + .contains_key_value("type", GdUnitReport.FAILURE) \ + .contains_key_value("line_number", 42) \ + .contains_key_value("message", "test message") + + +func test_serialize_report_without_error_has_no_stack_trace_key() -> void: + var report := GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "test message") + assert_bool(report.serialize().has("stack_trace")).is_false() + + +func test_serialize_report_with_error_includes_stack_trace_key() -> void: + var trace := GdUnitStackTrace.of([ + GdUnitStackTraceElement.new("res://test.gd", 10, "func_a"), + ]) + var serialized := GdUnitReport.new().from_error(GdUnitReport.FAILURE, GdUnitError.new("test message", 10, trace)).serialize() + assert_dict(serialized).contains_key_value("stack_trace", trace.serialize()) + + +func test_deserialize_without_stack_trace_produces_full_report() -> void: + var serialized := {"type": GdUnitReport.FAILURE, "line_number": 42, "message": "test message"} + assert_that(GdUnitReport.new().deserialize(serialized)).is_equal(GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "test message")) + + +func test_deserialize_reconstructs_full_report() -> void: + var trace := GdUnitStackTrace.of([ + GdUnitStackTraceElement.new("res://my_test.gd", 10, "func_a"), + GdUnitStackTraceElement.new("res://my_test.gd", 20, "func_b"), + ]) + var expected := GdUnitReport.new().from_error(GdUnitReport.FAILURE, GdUnitError.new("test message", 10, trace)) + var serialized := { + "type": GdUnitReport.FAILURE, + "line_number": 10, + "message": "test message", + "stack_trace": trace.serialize() + } + assert_that(GdUnitReport.new().deserialize(serialized)).is_equal(expected) + + +func test_serialize_deserialize_round_trip_preserves_full_report() -> void: + var original := GdUnitReport.new().from_error(GdUnitReport.FAILURE, + GdUnitError.new("test message", 42, GdUnitStackTrace.of([ + GdUnitStackTraceElement.new("res://my_test.gd", 42, "check_value"), + GdUnitStackTraceElement.new("res://my_test.gd", 55, "run_suite"), + ]))) + assert_that(GdUnitReport.new().deserialize(original.serialize())).is_equal(original) + +#endregion diff --git a/addons/gdUnit4/test/core/report/GdUnitReportTest.gd.uid b/addons/gdUnit4/test/core/report/GdUnitReportTest.gd.uid new file mode 100644 index 0000000..cf8d966 --- /dev/null +++ b/addons/gdUnit4/test/core/report/GdUnitReportTest.gd.uid @@ -0,0 +1 @@ +uid://hj5yde4adtm diff --git a/addons/gdUnit4/test/core/resources/Area4D.txt b/addons/gdUnit4/test/core/resources/Area4D.txt new file mode 100644 index 0000000..aa134c4 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/Area4D.txt @@ -0,0 +1,19 @@ +class Area4D extends Resource: + +const SOUND := 1 +const ATMOSPHERE := 2 +var _meta := Dictionary() + +func _init(_x :int, atmospere :AtmosphereData = null) -> void: + _meta[ATMOSPHERE] = atmospere + +func get_sound() -> SoundData: + # sounds are optional + if _meta.has(SOUND): + @warning_ignore("unsafe_cast") + return _meta[SOUND] as SoundData + return null + +func get_atmoshere() -> AtmosphereData: + @warning_ignore("unsafe_cast") + return _meta[ATMOSPHERE] as AtmosphereData diff --git a/addons/gdUnit4/test/core/resources/AtmosphereData.txt b/addons/gdUnit4/test/core/resources/AtmosphereData.txt new file mode 100644 index 0000000..2e34ac4 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/AtmosphereData.txt @@ -0,0 +1,22 @@ +class AtmosphereData: +enum { + WATER, + AIR, + SMOKY, +} +var _toxigen :float +var _type :int + +func _init(type := AIR, toxigen := 0.0) -> void: + _type = type + _toxigen = toxigen +# some comment, and an row staring with an space to simmulate invalid formatting + + +# seter func with default values +func set_data(type := AIR, toxigen := 0.0) -> void: + _type = type + _toxigen = toxigen + +static func to_atmosphere(_value :Dictionary) -> AtmosphereData: + return null diff --git a/addons/gdUnit4/test/core/resources/GdUnitRunner_old_format.cfg b/addons/gdUnit4/test/core/resources/GdUnitRunner_old_format.cfg new file mode 100644 index 0000000..cc6eece Binary files /dev/null and b/addons/gdUnit4/test/core/resources/GdUnitRunner_old_format.cfg differ diff --git a/addons/gdUnit4/test/core/resources/SoundData.txt b/addons/gdUnit4/test/core/resources/SoundData.txt new file mode 100644 index 0000000..91e62ba --- /dev/null +++ b/addons/gdUnit4/test/core/resources/SoundData.txt @@ -0,0 +1,5 @@ +class SoundData: +@warning_ignore("unused_private_class_variable") +var _sample :String +@warning_ignore("unused_private_class_variable") +var _randomnes :float diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_a.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_a.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_a.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_b.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_b.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_b.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_a.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_a.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_a.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_b.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_b.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_b.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_b/folder_ba/file_x.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_b/folder_ba/file_x.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_b/folder_ba/file_x.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_c/file_z.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_c/file_z.txt new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd new file mode 100644 index 0000000..75a0152 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd @@ -0,0 +1,7 @@ +class_name PascalCaseWithClassName +extends Resource + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd.uid b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd.uid new file mode 100644 index 0000000..63a0980 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd.uid @@ -0,0 +1 @@ +uid://dbsayjbpgca5s diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd new file mode 100644 index 0000000..c18eedd --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd @@ -0,0 +1,6 @@ +extends RefCounted + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd.uid b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd.uid new file mode 100644 index 0000000..a32b249 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd.uid @@ -0,0 +1 @@ +uid://d0ymte6dbmo4k diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/extends_on_same_line.gd b/addons/gdUnit4/test/core/resources/naming_conventions/extends_on_same_line.gd new file mode 100644 index 0000000..8c52b73 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/extends_on_same_line.gd @@ -0,0 +1,6 @@ +# we do explicit define the class name and extends in a single line +class_name ClassNameExtendsInSameLine extends Resource + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/extends_on_same_line.gd.uid b/addons/gdUnit4/test/core/resources/naming_conventions/extends_on_same_line.gd.uid new file mode 100644 index 0000000..0eb251f --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/extends_on_same_line.gd.uid @@ -0,0 +1 @@ +uid://4y1bdwskbs6s diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd new file mode 100644 index 0000000..ee74051 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd @@ -0,0 +1,7 @@ +class_name SnakeCaseWithClassName +extends Resource + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd.uid b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd.uid new file mode 100644 index 0000000..2cb28bc --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd.uid @@ -0,0 +1 @@ +uid://cjuqnakiv7aah diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd new file mode 100644 index 0000000..c18eedd --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd @@ -0,0 +1,6 @@ +extends RefCounted + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd.uid b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd.uid new file mode 100644 index 0000000..186397b --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd.uid @@ -0,0 +1 @@ +uid://flk4nqnvkh6o diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/ClassWithVariantTypeDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/ClassWithVariantTypeDefaultArguments.gd new file mode 100644 index 0000000..b4e3e39 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/ClassWithVariantTypeDefaultArguments.gd @@ -0,0 +1,8 @@ +extends RefCounted + +@warning_ignore("unused_parameter") +func add_button( + ucids: Array[int], position: Vector2i, size: Vector2i, style: int, text: Variant = "", + button_name := "", type_in := 0, caption := "", show_everywhere := false +) -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/ClassWithVariantTypeDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/ClassWithVariantTypeDefaultArguments.gd.uid new file mode 100644 index 0000000..79b0838 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/ClassWithVariantTypeDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://baimop8q2grja diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeBoolDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeBoolDefaultArguments.gd new file mode 100644 index 0000000..1cd8dc9 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeBoolDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeBoolDefaultArguments +extends RefCounted + + +func on_bool_case1(value: bool) -> bool: + return value + + +func on_bool_case2(value: bool = true) -> bool: + return value + + +func on_bool_case3(value := true) -> bool: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeBoolDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeBoolDefaultArguments.gd.uid new file mode 100644 index 0000000..5ccc520 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeBoolDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://dmuhyqa5wyh1o diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeFloatDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeFloatDefaultArguments.gd new file mode 100644 index 0000000..5540896 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeFloatDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeFloatDefaultArguments +extends RefCounted + + +func on_float_case1(value: float) -> float: + return value + + +func on_float_case2(value: float = 42.1) -> float: + return value + + +func on_float_case3(value := 42.1) -> float: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeFloatDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeFloatDefaultArguments.gd.uid new file mode 100644 index 0000000..a480e4c --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeFloatDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://d1fma1sxi8jlq diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeIntDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeIntDefaultArguments.gd new file mode 100644 index 0000000..46f816c --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeIntDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeIntDefaultArguments +extends RefCounted + + +func on_int_case1(value: int) -> int: + return value + + +func on_int_case2(value: int = 42) -> int: + return value + + +func on_int_case3(value := 42) -> int: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeIntDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeIntDefaultArguments.gd.uid new file mode 100644 index 0000000..1517201 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeIntDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://m2e54sfjok0k diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeNodePathDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeNodePathDefaultArguments.gd new file mode 100644 index 0000000..56ad3a2 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeNodePathDefaultArguments.gd @@ -0,0 +1,18 @@ +class_name ClassWithBasicTypeNodePathDefaultArguments +extends RefCounted + + +func on_node_path_case1(value: NodePath) -> NodePath: + return value + + +func on_node_path_case2(value: NodePath = "foo1") -> NodePath: + return value + + +func on_node_path_case3(value: NodePath = NodePath("foo2")) -> NodePath: + return value + + +func on_node_path_case4(value := NodePath(NodePath("foo3"))) -> NodePath: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeNodePathDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeNodePathDefaultArguments.gd.uid new file mode 100644 index 0000000..8ebaf6a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeNodePathDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://kkgmal5ovdim diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeRect2DefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeRect2DefaultArguments.gd new file mode 100644 index 0000000..d9193c1 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeRect2DefaultArguments.gd @@ -0,0 +1,22 @@ +class_name ClassWithBasicTypeRect2DefaultArguments +extends RefCounted + + +func on_rect2_case1(value: Rect2) -> Rect2: + return value + + +func on_rect2_case2(value: Rect2 = Rect2()) -> Rect2: + return value + + +func on_rect2_case3(value := Rect2()) -> Rect2: + return value + + +func on_rect2_case4(value := Rect2(Vector2.ONE, Vector2.ZERO)) -> Rect2: + return value + + +func on_rect2_case5(value := Rect2(0, 1, 2, 3)) -> Rect2: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeRect2DefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeRect2DefaultArguments.gd.uid new file mode 100644 index 0000000..87fedcf --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeRect2DefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://choshiyup5v0v diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringDefaultArguments.gd new file mode 100644 index 0000000..1bca145 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeStringDefaultArguments +extends RefCounted + + +func on_string_case1(value: String) -> String: + return value + + +func on_string_case2(value: String = "foo") -> String: + return value + + +func on_string_case3(value := "foo") -> String: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringDefaultArguments.gd.uid new file mode 100644 index 0000000..f458a09 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://ddqfxdp0lg78h diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringNameDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringNameDefaultArguments.gd new file mode 100644 index 0000000..bbbcf55 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringNameDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeStringNameDefaultArguments +extends RefCounted + + +func on_string_name_case1(value: StringName) -> StringName: + return value + + +func on_string_name_case2(value: StringName = &"foo") -> StringName: + return value + + +func on_string_name_case3(value := &"foo") -> StringName: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringNameDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringNameDefaultArguments.gd.uid new file mode 100644 index 0000000..50c5db3 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeStringNameDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://dvthgchd1obf1 diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeTransform2DDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeTransform2DDefaultArguments.gd new file mode 100644 index 0000000..9a18aeb --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeTransform2DDefaultArguments.gd @@ -0,0 +1,18 @@ +class_name ClassWithBasicTypeTransform2DDefaultArguments +extends RefCounted + + +func on_transform2d_case1(value: Transform2D) -> Transform2D: + return value + + +func on_transform2d_case2(value: Transform2D = Transform2D()) -> Transform2D: + return value + + +func on_transform2d_case3(value := Transform2D()) -> Transform2D: + return value + + +func on_transform2d_case4(value := Transform2D(1.2, Vector2.ONE)) -> Transform2D: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeTransform2DDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeTransform2DDefaultArguments.gd.uid new file mode 100644 index 0000000..4a1894e --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeTransform2DDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://8kuexxbk3wxf diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2DefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2DefaultArguments.gd new file mode 100644 index 0000000..40088ad --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2DefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeVector2DefaultArguments +extends RefCounted + + +func on_vector2_case1(value: Vector2) -> Vector2: + return value + + +func on_vector2_case2(value: Vector2 = Vector2.ONE) -> Vector2: + return value + + +func on_vector2_case3(value := Vector2.ONE) -> Vector2: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2DefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2DefaultArguments.gd.uid new file mode 100644 index 0000000..fdb4042 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2DefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://cjrmxtnpadhao diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2iDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2iDefaultArguments.gd new file mode 100644 index 0000000..1336b4c --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2iDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeVector2iDefaultArguments +extends RefCounted + + +func on_vector2i_case1(value: Vector2i) -> Vector2i: + return value + + +func on_vector2i_case2(value: Vector2i = Vector2i.ONE) -> Vector2i: + return value + + +func on_vector2i_case3(value := Vector2i.ONE) -> Vector2i: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2iDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2iDefaultArguments.gd.uid new file mode 100644 index 0000000..5c17086 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector2iDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://dkfd1rujqi46o diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3DefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3DefaultArguments.gd new file mode 100644 index 0000000..ca02fe3 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3DefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeVector3DefaultArguments +extends RefCounted + + +func on_vector3_case1(value: Vector3) -> Vector3: + return value + + +func on_vector3_case2(value: Vector3 = Vector3.ONE) -> Vector3: + return value + + +func on_vector3_case3(value := Vector3.ONE) -> Vector3: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3DefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3DefaultArguments.gd.uid new file mode 100644 index 0000000..5a3f2e4 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3DefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://dgwia6rf3vnlo diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3iDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3iDefaultArguments.gd new file mode 100644 index 0000000..90801fa --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3iDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeVector3iDefaultArguments +extends RefCounted + + +func on_vector3i_case1(value: Vector3i) -> Vector3i: + return value + + +func on_vector3i_case2(value: Vector3i = Vector3i.ONE) -> Vector3i: + return value + + +func on_vector3i_case3(value := Vector3i.ONE) -> Vector3i: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3iDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3iDefaultArguments.gd.uid new file mode 100644 index 0000000..f7f7178 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector3iDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://bruk6ris5htv1 diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4DefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4DefaultArguments.gd new file mode 100644 index 0000000..bb4c505 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4DefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeVector4DefaultArguments +extends RefCounted + + +func on_vector4_case1(value: Vector4) -> Vector4: + return value + + +func on_vector4_case2(value: Vector4 = Vector4.ONE) -> Vector4: + return value + + +func on_vector4_case3(value := Vector4.ONE) -> Vector4: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4DefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4DefaultArguments.gd.uid new file mode 100644 index 0000000..4a01e69 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4DefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://xxkdlx44r1t7 diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4iDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4iDefaultArguments.gd new file mode 100644 index 0000000..f79380e --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4iDefaultArguments.gd @@ -0,0 +1,14 @@ +class_name ClassWithBasicTypeVector4iDefaultArguments +extends RefCounted + + +func on_vector4i_case1(value: Vector4i) -> Vector4i: + return value + + +func on_vector4i_case2(value: Vector4i = Vector4i.ONE) -> Vector4i: + return value + + +func on_vector4i_case3(value := Vector4i.ONE) -> Vector4i: + return value diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4iDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4iDefaultArguments.gd.uid new file mode 100644 index 0000000..326483a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/basic_build_in_types/ClassWithBasicTypeVector4iDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://70y381sxxpdo diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithArrayDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithArrayDefaultArguments.gd new file mode 100644 index 0000000..de25774 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithArrayDefaultArguments.gd @@ -0,0 +1,70 @@ +class_name ClassWithArrayDefaultArguments +extends RefCounted + + +func on_array_case1(values: Array = []) -> Array: + return values + + +func on_array_case2(values := Array()) -> Array: + return values + + +func on_array_case3(values := []) -> Array: + return values + + +func on_array_case4(values := [1, "3", [], {}]) -> Array: + return values + + +func on_array_case5(values := [ + 1, + "3", + [], + {}]) -> Array: + return values + + +func on_array_case6(values := Array([1, "3", [], {}])) -> Array: + return values + + +func on_packed_byte_array(values := PackedByteArray()) -> Array: + return values + + +func on_packed_color_array(values := PackedColorArray()) -> Array: + return values + + +func on_packed_float32_array(values := PackedFloat32Array()) -> Array: + return values + + +func on_packed_float64_array(values := PackedFloat64Array()) -> Array: + return values + + +func on_packed_int32_array(values := PackedInt32Array()) -> Array: + return values + + +func on_packed_int64_array(values := PackedInt64Array()) -> Array: + return values + + +func on_packed_string_array(values := PackedStringArray()) -> Array: + return values + + +func on_packed_vector2_array(values := PackedVector2Array()) -> Array: + return values + + +func on_packed_vector3_array(values := PackedVector3Array()) -> Array: + return values + + +func on_packed_vector4_array(values := PackedVector4Array()) -> Array: + return values diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithArrayDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithArrayDefaultArguments.gd.uid new file mode 100644 index 0000000..8a8afba --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithArrayDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://bdswpvkpq8a5h diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithCallableDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithCallableDefaultArguments.gd new file mode 100644 index 0000000..5c93d9a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithCallableDefaultArguments.gd @@ -0,0 +1,18 @@ +class_name ClassWithCallableDefaultArguments +extends RefCounted + + +func on_callable_case1(cb: Callable) -> Callable: + return cb + + +func on_callable_case2(cb: Callable = Callable()) -> Callable: + return cb + + +func on_callable_case3(cb := Callable()) -> Callable: + return cb + + +func on_callable_case4(cb := Callable(null, "method_foo")) -> Callable: + return cb diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithCallableDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithCallableDefaultArguments.gd.uid new file mode 100644 index 0000000..516e4ef --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithCallableDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://dfvr0tcggkwfd diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithDictionaryDefaultArguments.gd b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithDictionaryDefaultArguments.gd new file mode 100644 index 0000000..46fffa4 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithDictionaryDefaultArguments.gd @@ -0,0 +1,28 @@ +class_name ClassWithDictionaryDefaultArguments +extends RefCounted + + +func on_dictionary_case1(dict: Dictionary = {}) -> Array: + return dict.keys() + + +func on_dictionary_case2(dict := Dictionary()) -> Array: + return dict.keys() + + +func on_dictionary_case3(dict := {}) -> Array: + return dict.keys() + + +func on_dictionary_case4(dict := {"a":"value_a", "b":"value_b"}) -> Array: + return dict.keys() + + +func on_dictionary_case5(dict := { + "a":"value_a", + "b":"value_b"}) -> Array: + return dict.keys() + + +func on_dictionary_case6(dict := Dictionary({"a":"value_a", "b":"value_b"})) -> Array: + return dict.keys() diff --git a/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithDictionaryDefaultArguments.gd.uid b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithDictionaryDefaultArguments.gd.uid new file mode 100644 index 0000000..7bda71a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/parsing/functions/container_build_in_types/ClassWithDictionaryDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://b4w2f2ydjuvg diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd new file mode 100644 index 0000000..0c7b8d6 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd @@ -0,0 +1,22 @@ +class_name BaseTest +extends GdUnitTestSuite + + +func before() -> void: + pass + + +func after() -> void: + pass + + +func before_test() -> void: + pass + + +func after_test() -> void: + pass + + +func test_foo1() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd.uid new file mode 100644 index 0000000..0d17a22 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd.uid @@ -0,0 +1 @@ +uid://c60t62da3ygfx diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd new file mode 100644 index 0000000..cd1313b --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd @@ -0,0 +1,14 @@ +class_name ExtendedTest +extends BaseTest + + +func before_test() -> void: + pass + + +func after_test() -> void: + pass + + +func test_foo2() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd.uid new file mode 100644 index 0000000..a749142 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd.uid @@ -0,0 +1 @@ +uid://cpdsyt6b3psnt diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd new file mode 100644 index 0000000..299a52d --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd @@ -0,0 +1,4 @@ +extends ExtendedTest + +func test_foo3() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd.uid new file mode 100644 index 0000000..8853dbc --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd.uid @@ -0,0 +1 @@ +uid://bu68l3k4mu8ro diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd new file mode 100644 index 0000000..17cfc19 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd @@ -0,0 +1,4 @@ +extends GdUnitTestSuite + +func test_foo1() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd.uid new file mode 100644 index 0000000..b4c0090 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd.uid @@ -0,0 +1 @@ +uid://de5i2agc63bos diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd new file mode 100644 index 0000000..eea2d4f --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd @@ -0,0 +1,4 @@ +extends "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd" + +func test_foo2() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd.uid new file mode 100644 index 0000000..f070c61 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd.uid @@ -0,0 +1 @@ +uid://bc7rbomnvj5wg diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd new file mode 100644 index 0000000..a14c8d3 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd @@ -0,0 +1,4 @@ +extends "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd" + +func test_foo3() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd.uid new file mode 100644 index 0000000..d209254 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd.uid @@ -0,0 +1 @@ +uid://cja6pyxgbqm4g diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd new file mode 100644 index 0000000..e0bbaae --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd @@ -0,0 +1,18 @@ +extends EditorInspectorPlugin + + +func _ready() -> void: + pass + + +func _process(_delta: float) -> void: + pass + + +func _can_handle(_object :Object) -> bool: + return true + + +@warning_ignore("unused_parameter") +func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool: + return false diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd.uid new file mode 100644 index 0000000..99251fa --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd.uid @@ -0,0 +1 @@ +uid://cmembxr8iyyg4 diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/plugin.cfg b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/plugin.cfg new file mode 100644 index 0000000..bc02de1 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="TestPlugin" +description="" +author="" +version="" +script="testplugin.gd" diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/preload.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/preload.gd new file mode 100644 index 0000000..82d4344 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/preload.gd @@ -0,0 +1,2 @@ + +var plugin := preload("res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd").new() diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/preload.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/preload.gd.uid new file mode 100644 index 0000000..d711409 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/preload.gd.uid @@ -0,0 +1 @@ +uid://d2xfcgb8ouneo diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/testplugin.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/testplugin.gd new file mode 100644 index 0000000..cc2e906 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/testplugin.gd @@ -0,0 +1,14 @@ +@tool +extends EditorPlugin + +var plugin: EditorInspectorPlugin + + +func _enter_tree() -> void: + plugin = preload("res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/inspector_plugin.gd").new() + add_inspector_plugin(plugin) + pass + + +func _exit_tree() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/testplugin.gd.uid b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/testplugin.gd.uid new file mode 100644 index 0000000..bc241fe --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/plugin/testplugin.gd.uid @@ -0,0 +1 @@ +uid://br72mdr8yc0vv diff --git a/addons/gdUnit4/test/core/resources/scenes/SceneWithEmbeddedScript.tscn b/addons/gdUnit4/test/core/resources/scenes/SceneWithEmbeddedScript.tscn new file mode 100644 index 0000000..88df0f9 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/SceneWithEmbeddedScript.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=2 format=3 uid="uid://t87enbab7q2c"] + +[sub_resource type="GDScript" id="GDScript_qh6va"] +script/source = "# build-in script +extends Control + +@warning_ignore(\"unused_private_class_variable\") +var _a: int +" + +[node name="SceneWithEmbeddedScript" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = SubResource("GDScript_qh6va") diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd new file mode 100644 index 0000000..6b502b9 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd @@ -0,0 +1,45 @@ +extends PanelContainer + + +# Godot calls this method to get data that can be dragged and dropped onto controls that expect drop data. +# Returns null if there is no data to drag. +# Controls that want to receive drop data should implement can_drop_data() and drop_data(). +# position is local to this control. Drag may be forced with force_drag(). +func _get_drag_data(_position: Vector2) -> Variant: + var x :TextureRect = $TextureRect + var data: = {texture = x.texture} + var drag_texture :TextureRect = x.duplicate() + drag_texture.size = x.size + drag_texture.position = x.global_position * -0.2 + + # set drag preview + var control := Panel.new() + control.add_child(drag_texture) + # center texture relative to mouse pos + set_drag_preview(control) + return data + + +# Godot calls this method to test if data from a control's get_drag_data() can be dropped at position. position is local to this control. +func _can_drop_data(_position: Vector2, data :Variant) -> bool: + @warning_ignore("unsafe_method_access") + return typeof(data) == TYPE_DICTIONARY and data.has("texture") + + +# Godot calls this method to pass you the data from a control's get_drag_data() result. +# Godot first calls can_drop_data() to test if data is allowed to drop at position where position is local to this control. +func _drop_data(_position: Vector2, data :Variant) -> void: + var drag_texture :Texture = data["texture"] + if drag_texture != null: + ($TextureRect as TextureRect).texture = drag_texture + + +# Virtual method to be implemented by the user. Use this method to process and accept inputs on UI elements. See accept_event(). +func _gui_input(_event :InputEvent) -> void: + #if _event is InputEventScreenDrag: + # prints(" InputEventScreenDrag", _event.as_text()) + #if _event is InputEventScreenTouch: + # prints(" InputEventScreenTouch", _event.as_text()) + #if _event is InputEventMouseButton: + # prints("Panel _gui_input", _event.as_text()) + pass diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd.uid b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd.uid new file mode 100644 index 0000000..4cf614f --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd.uid @@ -0,0 +1 @@ +uid://0lj742if8267 diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn new file mode 100644 index 0000000..0a1032c --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn @@ -0,0 +1,23 @@ +[gd_scene load_steps=3 format=3 uid="uid://ca2rr3dan4vvw"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd" id="1"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_s2qi1"] +size = Vector2(110, 110) + +[node name="Panel" type="PanelContainer"] +offset_left = 245.0 +offset_top = 232.0 +offset_right = 350.0 +offset_bottom = 337.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="TextureRect" type="TextureRect" parent="."] +layout_mode = 2 +expand_mode = 1 + +[node name="touch_area" type="TouchScreenButton" parent="TextureRect"] +position = Vector2(52, 52) +shape = SubResource("RectangleShape2D_s2qi1") diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd new file mode 100644 index 0000000..342f5b0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd @@ -0,0 +1,22 @@ +extends Control + +@onready var texture := preload("res://addons/gdUnit4/test/core/resources/scenes/icon.png") + +func _ready() -> void: + # set initial drag texture + ($left/TextureRect as TextureRect).texture = texture + + +# Virtual method to be implemented by the user. Use this method to process and accept inputs on UI elements. See accept_event(). +func _gui_input(_event :InputEvent) -> void: + #prints("Game _gui_input", _event.as_text()) + pass + + +func _on_Button_button_down() -> void: + # print("BUTTON DOWN") + pass + + +func _on_quit_pressed() -> void: + get_tree().quit() diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd.uid b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd.uid new file mode 100644 index 0000000..6fd4e02 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd.uid @@ -0,0 +1 @@ +uid://j7evr1abtw21 diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn new file mode 100644 index 0000000..5ad827e --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn @@ -0,0 +1,51 @@ +[gd_scene load_steps=3 format=3 uid="uid://skueh3d5qn46"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd" id="1"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn" id="2_u5ccv"] + +[node name="DragAndDropScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="left" parent="." instance=ExtResource("2_u5ccv")] +layout_mode = 0 +offset_left = 250.0 +offset_top = 240.0 +offset_right = 355.0 +offset_bottom = 345.0 +localize_numeral_system = false +metadata/_edit_use_anchors_ = true + +[node name="right" parent="." instance=ExtResource("2_u5ccv")] +layout_mode = 0 +offset_left = 370.0 +offset_top = 240.0 +offset_right = 475.0 +offset_bottom = 345.0 + +[node name="Button" type="Button" parent="."] +layout_mode = 0 +offset_left = 243.0 +offset_top = 40.0 +offset_right = 479.0 +offset_bottom = 200.0 +text = "BUTTON" +metadata/_edit_use_anchors_ = true + +[node name="Button2" type="Button" parent="."] +layout_mode = 0 +offset_left = 593.0 +offset_top = 201.0 +offset_right = 700.0 +offset_bottom = 239.0 +text = "QUIT" + +[connection signal="button_down" from="Button" to="." method="_on_Button_button_down"] +[connection signal="pressed" from="Button2" to="." method="_on_quit_pressed"] diff --git a/addons/gdUnit4/test/core/resources/scenes/icon.png b/addons/gdUnit4/test/core/resources/scenes/icon.png new file mode 100644 index 0000000..12de79f Binary files /dev/null and b/addons/gdUnit4/test/core/resources/scenes/icon.png differ diff --git a/addons/gdUnit4/test/core/resources/scenes/icon.png.import b/addons/gdUnit4/test/core/resources/scenes/icon.png.import new file mode 100644 index 0000000..5935001 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ctf1j751f04v8" +path="res://.godot/imported/icon.png-29a1278f096c68265f00742f13915f1d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/test/core/resources/scenes/icon.png" +dest_files=["res://.godot/imported/icon.png-29a1278f096c68265f00742f13915f1d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd new file mode 100644 index 0000000..6922051 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd @@ -0,0 +1,16 @@ +extends Control + +var _player_jump_action_released := false + +# enable for manual testing +func __init() -> void: + var event := InputEventKey.new() + event.keycode = KEY_SPACE + InputMap.add_action("player_jump") + InputMap.action_add_event("player_jump", event) + + +func _input(_event :InputEvent) -> void: + if InputMap.has_action("player_jump"): + _player_jump_action_released = Input.is_action_just_released("player_jump", true) + #prints("_input2:player_jump", Input.is_action_just_released("player_jump"), Input.is_action_just_released("player_jump", true)) diff --git a/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd.uid b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd.uid new file mode 100644 index 0000000..8306469 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd.uid @@ -0,0 +1 @@ +uid://cu6n33q55buob diff --git a/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn new file mode 100644 index 0000000..a5622b3 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://cvklb72mxqh1h"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd" id="1_wslmn"] + +[node name="InputEventTestScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_wslmn") diff --git a/addons/gdUnit4/test/core/resources/scenes/scene_audio.tscn b/addons/gdUnit4/test/core/resources/scenes/scene_audio.tscn new file mode 100644 index 0000000..aa19c60 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/scene_audio.tscn @@ -0,0 +1,5 @@ +[gd_scene format=3 uid="uid://bi4lvq6pg2w81"] + +[node name="SceneAudio" type="Node2D"] + +[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="."] diff --git a/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd b/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd new file mode 100644 index 0000000..801f5d3 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd @@ -0,0 +1,15 @@ +extends Node2D + +class Player extends Node: + var position :Vector3 = Vector3.ZERO + + + func _init() -> void: + set_name("Player") + + func is_on_floor() -> bool: + return true + + +func _ready() -> void: + add_child(Player.new(), true) diff --git a/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd.uid b/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd.uid new file mode 100644 index 0000000..d136e2b --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd.uid @@ -0,0 +1 @@ +uid://cpqf4yet0um1q diff --git a/addons/gdUnit4/test/core/resources/scenes/simple_scene.scn b/addons/gdUnit4/test/core/resources/scenes/simple_scene.scn new file mode 100644 index 0000000..e6bf98b Binary files /dev/null and b/addons/gdUnit4/test/core/resources/scenes/simple_scene.scn differ diff --git a/addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn b/addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn new file mode 100644 index 0000000..01a2bfd --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=3 uid="uid://cn8ucy2rheu0f"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/simple_scene.gd" id="2"] +[ext_resource type="Texture2D" path="res://addons/gdUnit4/test/core/resources/scenes/icon.png" id="2_jtr47"] + +[node name="Node2D" type="Node2D"] +script = ExtResource("2") + +[node name="Sprite2D" type="Sprite2D" parent="."] +position = Vector2(504, 252) +texture = ExtResource("2_jtr47") diff --git a/addons/gdUnit4/test/core/resources/script_with_class_name.gd b/addons/gdUnit4/test/core/resources/script_with_class_name.gd new file mode 100644 index 0000000..658937b --- /dev/null +++ b/addons/gdUnit4/test/core/resources/script_with_class_name.gd @@ -0,0 +1,6 @@ +class_name ScriptWithClassName +extends Resource + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/script_with_class_name.gd.uid b/addons/gdUnit4/test/core/resources/script_with_class_name.gd.uid new file mode 100644 index 0000000..29e5d48 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/script_with_class_name.gd.uid @@ -0,0 +1 @@ +uid://ciriincdjlrhy diff --git a/addons/gdUnit4/test/core/resources/script_without_class_name.gd b/addons/gdUnit4/test/core/resources/script_without_class_name.gd new file mode 100644 index 0000000..09cc3d5 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/script_without_class_name.gd @@ -0,0 +1,5 @@ +extends RefCounted + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/script_without_class_name.gd.uid b/addons/gdUnit4/test/core/resources/script_without_class_name.gd.uid new file mode 100644 index 0000000..591c36f --- /dev/null +++ b/addons/gdUnit4/test/core/resources/script_without_class_name.gd.uid @@ -0,0 +1 @@ +uid://7brhw8l1ma6i diff --git a/addons/gdUnit4/test/core/resources/sources/test_person.gd b/addons/gdUnit4/test/core/resources/sources/test_person.gd new file mode 100644 index 0000000..449ebe5 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/sources/test_person.gd @@ -0,0 +1,17 @@ +extends RefCounted + +var _first_name :String +var _last_name :String + +func _init(first_name_ :String, last_name_ :String) -> void: + _first_name = first_name_ + _last_name = last_name_ + +func first_name() -> String: + return _first_name + +func last_name() -> String: + return _last_name + +func fully_name() -> String: + return _first_name + " " + _last_name diff --git a/addons/gdUnit4/test/core/resources/sources/test_person.gd.uid b/addons/gdUnit4/test/core/resources/sources/test_person.gd.uid new file mode 100644 index 0000000..c974073 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/sources/test_person.gd.uid @@ -0,0 +1 @@ +uid://c83qqrfjcv8x8 diff --git a/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd b/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd new file mode 100644 index 0000000..2b78370 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd @@ -0,0 +1,49 @@ +extends Node + + +func test_no_args() -> void: + pass + +@warning_ignore("unused_parameter") +func test_with_timeout(timeout:=2000) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_with_fuzzer(fuzzer := Fuzzers.rangei(-10, 22)) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_with_fuzzer_iterations(fuzzer := Fuzzers.rangei(-10, 22), fuzzer_iterations := 10) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_with_multible_fuzzers(fuzzer_a := Fuzzers.rangei(-10, 22), fuzzer_b := Fuzzers.rangei(23, 42), fuzzer_iterations := 10) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_multiline_arguments_a(fuzzer_a := Fuzzers.rangei(-10, 22), fuzzer_b := Fuzzers.rangei(23, 42), + fuzzer_iterations := 42) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_multiline_arguments_b( + fuzzer_a := Fuzzers.rangei(-10, 22), + fuzzer_b := Fuzzers.rangei(23, 42), + fuzzer_iterations := 23 + ) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_multiline_arguments_c( + timeout:=2000, + fuzzer_a := Fuzzers.rangei(-10, 22), + fuzzer_b := Fuzzers.rangei(23, 42), + fuzzer_iterations := 33 + ) -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd.uid b/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd.uid new file mode 100644 index 0000000..fb18c20 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd.uid @@ -0,0 +1 @@ +uid://4wi61vlpdmwb diff --git a/addons/gdUnit4/test/core/resources/testsuites/.gdignore b/addons/gdUnit4/test/core/resources/testsuites/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource b/addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource new file mode 100644 index 0000000..859e1f6 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource @@ -0,0 +1,48 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite +@warning_ignore('unused_parameter') + +var _stack : Array + +func before() -> void: + # init the stack + _stack = [] + +func before_test() -> void: + # clean the stack before every test run + _stack.clear() + + +@warning_ignore('unused_parameter') +func test_multi_yielding_with_fuzzer(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations := 10) -> void: + # verify the used stack is cleaned by 'before_test' + assert_array(_stack).is_empty() + _stack.push_back(1) + + prints("test iteration %d" % fuzzer.iteration_index()) + prints("4") + await get_tree().process_frame + prints("3") + await get_tree().process_frame + prints("2") + await get_tree().process_frame + prints("1") + await get_tree().process_frame + prints("Go") + +@warning_ignore('unused_parameter') +func test_multi_yielding_with_fuzzer_fail_after_3_iterations(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations := 10) -> void: + prints("test iteration %d" % fuzzer.iteration_index()) + # should never be greater than 3 because we interuppted after three iterations + assert_int(fuzzer.iteration_index()).is_less_equal(3) + prints("4") + await get_tree().process_frame + prints("3") + await get_tree().process_frame + prints("2") + await get_tree().process_frame + prints("1") + await get_tree().process_frame + prints("Go") + if fuzzer.iteration_index() >= 3: + assert_bool(true).is_false() diff --git a/addons/gdUnit4/test/core/resources/testsuites/NotATestSuite.cs b/addons/gdUnit4/test/core/resources/testsuites/NotATestSuite.cs new file mode 100644 index 0000000..8ae6826 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/NotATestSuite.cs @@ -0,0 +1,12 @@ +namespace GdUnit4.Tests.Resources; +#if GDUNIT4NET_API_V5 +using static Assertions; + +// will be ignored because of missing `[TestSuite]` anotation +public class NotATestSuite +{ + [TestCase] + public void TestFoo() => AssertBool(true).IsEqual(false); +} + +#endif diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestCaseFlaky.resource b/addons/gdUnit4/test/core/resources/testsuites/TestCaseFlaky.resource new file mode 100644 index 0000000..d319c92 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestCaseFlaky.resource @@ -0,0 +1,147 @@ +extends GdUnitTestSuite + + +var test_retries := { + "test_flaky_success" = 0, + "test_flaky_fail" = 0, + "test_success" = 0, + "test_parameterized_flaky:0" = 0, + "test_parameterized_flaky:1" = 0, + "test_parameterized_flaky:2" = 0, + "test_parameterized_flaky:3" = 0, + "test_parameterized_flaky:4" = 0, + "test_parameterized_flaky:5" = 0, + "test_fuzzed_flaky_success" = 0, + "test_fuzzed_flaky_fail" = 0 +} + +var _run_with_retries: int = GdUnitSettings.get_flaky_max_retries() + + +class ValueSetFuzzer extends Fuzzer: + var _values := [0,1,2,3,4] + + func next_value() -> Variant: + return _values.pop_front() + + +func before() -> void: + prints("run with max retries", _run_with_retries) + + +func after() -> void: + var retry_count :int = test_retries["test_flaky_success"] + assert_int(retry_count)\ + .override_failure_message("Expecting 3 retries to succeed for test 'test_flaky_success'\n but was %d" % retry_count)\ + .is_equal(3) + retry_count = test_retries["test_flaky_fail"] + assert_int(retry_count)\ + .override_failure_message("Expecting %d retries for test 'test_flaky_fail'\n but was %d" % [_run_with_retries, retry_count])\ + .is_equal(_run_with_retries) + retry_count = test_retries["test_success"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_success'\n but was %d" % retry_count)\ + .is_equal(1) + # verify retry count of paramaterized test + retry_count = test_retries["test_parameterized_flaky:0"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_parameterized_flaky:0'\n but was %d" % retry_count)\ + .is_equal(1) + retry_count = test_retries["test_parameterized_flaky:1"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_parameterized_flaky:1'\n but was %d" % retry_count)\ + .is_equal(1) + retry_count = test_retries["test_parameterized_flaky:2"] + assert_int(retry_count)\ + .override_failure_message("Expecting %d test iteration to fail 'test_parameterized_flaky:2'\n but was %d" % [_run_with_retries, retry_count])\ + .is_equal(_run_with_retries) + retry_count = test_retries["test_parameterized_flaky:3"] + assert_int(retry_count)\ + .override_failure_message("Expecting one test iteration to succeed 'test_parameterized_flaky:3'\n but was %d" % retry_count)\ + .is_equal(1) + retry_count = test_retries["test_parameterized_flaky:4"] + assert_int(retry_count)\ + .override_failure_message("Expecting 3 test iteration to succeed 'test_parameterized_flaky:4'\n but was %d" % retry_count)\ + .is_equal(3) + # fuzzed tests + retry_count = test_retries["test_fuzzed_flaky_success"] + assert_int(retry_count)\ + .override_failure_message("Expecting 3 retries to succeed for test 'test_fuzzed_flaky_success'\n but was %d" % retry_count)\ + .is_equal(3) + retry_count = test_retries["test_fuzzed_flaky_fail"] + assert_int(retry_count)\ + .override_failure_message("Expecting %d retries for test 'test_fuzzed_flaky_fail'\n but was %d" % [_run_with_retries, retry_count])\ + .is_equal(_run_with_retries) + + +func test_flaky_success() -> void: + test_retries["test_flaky_success"] += 1 + var retry_count :int = test_retries["test_flaky_success"] + # do retry between 1 and 3 + assert_int(retry_count).is_less_equal(3) + if retry_count <= 2: + fail("failure 1: failed at retry %d" % retry_count) + fail("failure 2: failed at retry %d" % retry_count) + + +func test_flaky_fail() -> void: + test_retries["test_flaky_fail"] += 1 + var retry_count :int = test_retries["test_flaky_fail"] + # do retry between 1 and 5 + assert_int(retry_count).is_less_equal(6) + if retry_count < 6: + fail("failed on test retry %d" % retry_count) + + +func test_success() -> void: + test_retries["test_success"] += 1 + var retry_count :int = test_retries["test_success"] + # do retry only one time + assert_int(retry_count).is_equal(1) + assert_bool(true).is_true() + + +@warning_ignore("unused_parameter") +func test_parameterized_flaky(test_index: int, expected_retry_count :int, test_parameters := [ + [0, 1], + [1, 1], + [2, 6], + [3, 1], + [4, 3]]) -> void: + + var test_case_name := "test_parameterized_flaky:%d" % test_index + test_retries[test_case_name] += 1 + var retry_count :int = test_retries[test_case_name] + assert_int(retry_count).is_less_equal(expected_retry_count) + + if test_index == 2 or test_index == 4: + # do fail if retry_count less expected count to fail + if retry_count < expected_retry_count: + fail("failed at retry %d" % retry_count) + + +@warning_ignore("unused_parameter") +func test_fuzzed_flaky_success(fuzzer := ValueSetFuzzer.new(), fuzzer_iterations := 5) -> void: + var fuzzer_value :int = fuzzer.next_value() + if fuzzer_value == 0: + test_retries["test_fuzzed_flaky_success"] += 1 + var retry_count :int = test_retries["test_fuzzed_flaky_success"] + # do retry between 1 and 3 + assert_int(retry_count).is_less_equal(3) + + if retry_count <= 2: + fail("failure 1: failed at retry %d" % retry_count) + fail("failure 2: failed at retry %d" % retry_count) + + +@warning_ignore("unused_parameter") +func test_fuzzed_flaky_fail(fuzzer := ValueSetFuzzer.new(), fuzzer_iterations := 5) -> void: + var fuzzer_value :int = fuzzer.next_value() + if fuzzer_value == 0: + test_retries["test_fuzzed_flaky_fail"] += 1 + var retry_count :int = test_retries["test_fuzzed_flaky_fail"] + # do retry between 1 and 3 + assert_int(retry_count).is_less_equal(6) + + if retry_count < 6: + fail("failed at retry %d" % retry_count) diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource b/addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource new file mode 100644 index 0000000..f25f174 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource @@ -0,0 +1,11 @@ +extends GdUnitTestSuite + + +@warning_ignore('unused_parameter') +func test_case1(timeout := 1000, do_skip:=1==1, skip_reason:="do not run this") -> void: + pass + + +@warning_ignore('unused_parameter') +func test_case2(skip_reason:="ignored") -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource new file mode 100644 index 0000000..93a8ce6 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource @@ -0,0 +1,20 @@ +# this test suite ends with success, no failures or errors +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").is_equal("suite after") + +func before_test() -> void: + assert_str("test before").is_equal("test before") + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").is_equal("test_case1") + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource new file mode 100644 index 0000000..0e9fb6d --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource @@ -0,0 +1,24 @@ +# this test suite ends with error on testcase1 by a timeout +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").is_equal("suite after") + +func before_test() -> void: + assert_str("test before").is_equal("test before") + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +# configure test with timeout of 2s +@warning_ignore('unused_parameter') +func test_case1(timeout:=2000) -> void: + assert_str("test_case1").is_equal("test_case1") + # wait 3s to let the test fail by timeout + await await_millis(3000) + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource new file mode 100644 index 0000000..c49b568 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource @@ -0,0 +1,12 @@ +# this test suite fails if (https://github.com/MikeSchulze/gdUnit4/issues/106) not fixed on iterating over testcases +extends GdUnitTestSuite + +func before() -> void: + @warning_ignore("unsafe_cast") + add_child(auto_free(Node.new()) as Node) + +func test_case1() -> void: + assert_str("test_case1").is_equal("test_case1") + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource new file mode 100644 index 0000000..a9d68d1 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource @@ -0,0 +1,21 @@ +# this test suite fails on multiple stages +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").override_failure_message("failed on after()").is_empty() + +func before_test() -> void: + assert_str("test before").override_failure_message("failed on before_test()").is_empty() + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").override_failure_message("failed 1 on test_case1()").is_empty() + assert_str("test_case1").override_failure_message("failed 2 on test_case1()").is_empty() + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource new file mode 100644 index 0000000..edeb324 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage after() +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").override_failure_message("failed on after()").is_empty() + +func before_test() -> void: + assert_str("test before").is_equal("test before") + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").is_equal("test_case1") + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource new file mode 100644 index 0000000..456cea3 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage after_test() +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").is_equal("suite after") + +func before_test() -> void: + assert_str("test before").is_equal("test before") + +func after_test() -> void: + assert_str("test after").override_failure_message("failed on after_test()").is_empty() + +func test_case1() -> void: + assert_str("test_case1").is_equal("test_case1") + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource new file mode 100644 index 0000000..d8afc3d --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage before() +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").override_failure_message("failed on before()").is_empty() + +func after() -> void: + assert_str("suite after").is_equal("suite after") + +func before_test() -> void: + assert_str("test before").is_equal("test before") + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").is_equal("test_case1") + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource new file mode 100644 index 0000000..615ac2e --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage before_test() +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").is_equal("suite after") + +func before_test() -> void: + assert_str("test before").override_failure_message("failed on before_test()").is_empty() + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").is_equal("test_case1") + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource new file mode 100644 index 0000000..c94d3c4 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource @@ -0,0 +1,20 @@ +# this test suite fails on a single test case +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").is_equal("suite after") + +func before_test() -> void: + assert_str("test before").is_equal("test before") + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").override_failure_message("failed on test_case1()").is_empty() + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.gd b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.gd new file mode 100644 index 0000000..c7c9327 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.gd @@ -0,0 +1,78 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite +@warning_ignore('unused_parameter') + + +class TestCaseStatistics: + var _testcase_before_called := 0 + var _testcase_after_called := 0 + var _test_called := 0 + var _expected_calls :int + + func _init(expected_calls :int) -> void: + _expected_calls = expected_calls + + func count_test_before_test() -> void: + _testcase_before_called +=1 + + func count_test_after_test() -> void: + _testcase_after_called +=1 + + func count_test() -> void: + _test_called += 1 + + +var _metrics: Dictionary[String, TestCaseStatistics] = { + "test_execute_3times" : TestCaseStatistics.new(3), + "test_execute_5times" : TestCaseStatistics.new(5) +} + +var _stack : Array +var _before_called := 0 +var _after_called := 0 + + +func before() -> void: + _before_called += 1 + # init the stack + _stack = [] + + +func after() -> void: + _after_called += 1 + assert_that(_before_called)\ + .override_failure_message("Expecting 'before' is called only one times")\ + .is_equal(1) + assert_that(_after_called)\ + .override_failure_message("Expecting 'after' is called only one times")\ + .is_equal(1) + + for test_case :String in _metrics.keys(): + var statistics: TestCaseStatistics = _metrics[test_case] + assert_int(statistics._testcase_before_called)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_before_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._test_called)\ + .override_failure_message("Expect test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._test_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._testcase_after_called)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_after_called, test_case])\ + .is_equal(statistics._expected_calls) + + +func before_test() -> void: + _metrics[__active_test_case].count_test_before_test() + # clean the stack before every test run + _stack.clear() + + +func after_test() -> void: + _metrics[__active_test_case].count_test_after_test() + + +func test_execute_3times(_fuzzer := Fuzzers.rangei(0, 1000), _fuzzer_iterations := 3) -> void: + _metrics[__active_test_case].count_test() + + +func test_execute_5times(_fuzzer := Fuzzers.rangei(0, 1000), _fuzzer_iterations := 5) -> void: + _metrics[__active_test_case].count_test() diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource new file mode 100644 index 0000000..45358b6 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource @@ -0,0 +1,56 @@ +class_name TestSuiteInvalidParameterizedTests +extends GdUnitTestSuite + +func test_no_parameters() -> void: + assert_that(true).is_equal(true) + +@warning_ignore('unused_parameter') +func test_parameterized_success(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] ]) -> void: + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_failed(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]) -> void: + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_to_less_args(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]) -> void: + pass + +@warning_ignore('unused_parameter') +func test_parameterized_to_many_args(a: int, b :int, c :int, d :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]) -> void: + pass + +@warning_ignore('unused_parameter') +func test_parameterized_to_less_args_at_index_1(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 6], + [3, 4, 5, 11], + [6, 7, 21] ]) -> void: + pass + +@warning_ignore('unused_parameter') +func test_parameterized_invalid_struct(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 6], + ["foo"], + [6, 7, 21] ]) -> void: + pass + +@warning_ignore('unused_parameter') +func test_parameterized_invalid_args(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 6], + [3, "4", 11], + [6, 7, 21] ]) -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.gd b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.gd new file mode 100644 index 0000000..26f13d7 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.gd @@ -0,0 +1,80 @@ +extends GdUnitTestSuite + + +class TestCaseStatistics: + var _testcase_before_called := 0 + var _testcase_after_called := 0 + var _test_called := 0 + var _expected_calls :int + + func _init(expected_calls :int) -> void: + _expected_calls = expected_calls + + func count_test_before_test() -> void: + _testcase_before_called +=1 + + func count_test_after_test() -> void: + _testcase_after_called +=1 + + func count_test() -> void: + _test_called += 1 + + +var _metrics: Dictionary[String, TestCaseStatistics] = { + "test_parameterized_2times" : TestCaseStatistics.new(2), + "test_parameterized_5times" : TestCaseStatistics.new(5) +} + +var _before_called := 0 +var _after_called := 0 + + +func before() -> void: + _before_called += 1 + + +func after() -> void: + _after_called += 1 + assert_that(_before_called)\ + .override_failure_message("Expecting 'before' is called only one times")\ + .is_equal(1) + assert_that(_after_called)\ + .override_failure_message("Expecting 'after' is called only one times")\ + .is_equal(1) + + for test_case :String in _metrics.keys(): + var statistics: TestCaseStatistics = _metrics[test_case] + assert_int(statistics._testcase_before_called)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_before_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._test_called)\ + .override_failure_message("Expect test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._test_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._testcase_after_called)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_after_called, test_case])\ + .is_equal(statistics._expected_calls) + + +func before_test() -> void: + _metrics[__active_test_case].count_test_before_test() + + +func after_test() -> void: + _metrics[__active_test_case].count_test_after_test() + + +func test_parameterized_2times(_value: int, _expected: bool, _test_parameters := [ + [0, false], + [1, true]]) -> void: + + _metrics[__active_test_case].count_test() + + +func test_parameterized_5times(_value: int, _expected: bool, _test_parameters := [ + [0, false], + [1, true], + [0, false], + [1, true], + [1, true]]) -> void: + + _metrics[__active_test_case].count_test() diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource new file mode 100644 index 0000000..b89719e --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource @@ -0,0 +1,148 @@ +extends GdUnitTestSuite + +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + + +class TestCaseStatistics: + var _testcase_before_called := 0 + var _testcase_after_called := 0 + var _expected_testcase_before :int + var _expected_testcase_after :int + + func _init(testcase_before_calls := 0, testcase_after_calls := 0) -> void: + _expected_testcase_before = testcase_before_calls + _expected_testcase_after = testcase_after_calls + + func count_test_before_test() -> void: + _testcase_before_called +=1 + + func count_test_after_test() -> void: + _testcase_after_called +=1 + + +var _metrics := { + "test_parameterized_bool_value" : TestCaseStatistics.new(2, 2), + "test_parameterized_int_values" : TestCaseStatistics.new(3, 3) +} + +var _before_called := 0 +var _after_called := 0 + + +func before() -> void: + _before_called += 1 + + +func after() -> void: + _after_called += 1 + assert_that(_before_called)\ + .override_failure_message("Expecting 'before' is called only one times")\ + .is_equal(1) + assert_that(_after_called)\ + .override_failure_message("Expecting 'after' is called only one times")\ + .is_equal(1) + + for test_case :String in _metrics.keys(): + var statistics: TestCaseStatistics = _metrics[test_case] + assert_int(statistics._testcase_before_called)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [statistics._expected_testcase_before, statistics._testcase_before_called, test_case])\ + .is_equal(statistics._expected_testcase_before) + assert_int(statistics._testcase_after_called)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [statistics._expected_testcase_after, statistics._testcase_after_called, test_case])\ + .is_equal(statistics._expected_testcase_after) + + +func before_test() -> void: + if _metrics.has(__active_test_case): + _metrics[__active_test_case].count_test_before_test() + + +func after_test() -> void: + if _metrics.has(__active_test_case): + _metrics[__active_test_case].count_test_after_test() + + +@warning_ignore('unused_parameter') +func test_parameterized_bool_value(a: int, expected :bool, test_parameters := [ + [0, false], + [1, true]]) -> void: + + assert_that(bool(a)).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_int_values(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] ]) -> void: + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_int_values_fail(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 22] ]) -> void: + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_float_values(a: float, b :float, expected :float, test_parameters := [ + [2.2, 2.2, 4.4], + [2.2, 2.3, 4.5], + [3.3, 2.2, 5.5] ]) -> void: + + assert_float(a+b).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_string_values(a: String, b :String, expected :String, test_parameters := [ + ["2.2", "2.2", "2.22.2"], + ["foo", "bar", "foobar"], + ["a", "b", "ab"] ]) -> void: + + assert_that(a+b).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_Vector2_values(a: Vector2, b :Vector2, expected :Vector2, test_parameters := [ + [Vector2.ONE, Vector2.ONE, Vector2(2, 2)], + [Vector2.LEFT, Vector2.RIGHT, Vector2.ZERO], + [Vector2.ZERO, Vector2.LEFT, Vector2.LEFT] ]) -> void: + + assert_that(a+b).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_Vector3_values(a: Vector3, b :Vector3, expected :Vector3, test_parameters := [ + [Vector3.ONE, Vector3.ONE, Vector3(2, 2, 2)], + [Vector3.LEFT, Vector3.RIGHT, Vector3.ZERO], + [Vector3.ZERO, Vector3.LEFT, Vector3.LEFT] ]) -> void: + + assert_that(a+b).is_equal(expected) + +class TestObj extends Resource: + var _value :String + + func _init(value :String) -> void: + _value = value + + func _to_string() -> String: + return _value + +@warning_ignore('unused_parameter') +func test_parameterized_obj_values(a: Object, b :Object, expected :String, test_parameters := [ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"]]) -> void: + + assert_that(a.to_string()+b.to_string()).is_equal(expected) + + +@warning_ignore('unused_parameter') +func test_dictionary_div_number_types( + value : Dictionary, + expected : Dictionary, + test_parameters : Array = [ + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50, bottom = 50, left = 50, right = 50}], + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50, bottom = 50, left = 50, right = 50}], + ] + ) -> void: + assert_that(value).is_equal(expected) diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource new file mode 100644 index 0000000..d99225a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource @@ -0,0 +1,20 @@ +extends GdUnitTestSuite + + +func is_skipped() -> bool: + return true + + +@warning_ignore('unused_parameter') +func before(do_skip:=is_skipped(), skip_reason:="do not run this") -> void: + pass + + +@warning_ignore('unused_parameter') +func test_case1(timeout := 1000, do_skip:=1==1, skip_reason:="do not run this") -> void: + pass + + +@warning_ignore('unused_parameter') +func test_case2(skip_reason:="ignored") -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteWithoutTests.gd b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteWithoutTests.gd new file mode 100644 index 0000000..2b72cbd --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteWithoutTests.gd @@ -0,0 +1,9 @@ +extends GdUnitTestSuite + + +func before() -> void: + pass + + +func foo() -> void: + pass diff --git a/addons/gdUnit4/test/core/runners/GdUnitTestCIRunnerTest.gd b/addons/gdUnit4/test/core/runners/GdUnitTestCIRunnerTest.gd new file mode 100644 index 0000000..f5e54a0 --- /dev/null +++ b/addons/gdUnit4/test/core/runners/GdUnitTestCIRunnerTest.gd @@ -0,0 +1,92 @@ +extends GdUnitTestSuite + + +const DiscoverExampleTestSuite : GDScript = preload("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + +func test_discover_tests_on_path() -> void: + var runner: GdUnitTestCIRunner = auto_free(GdUnitTestCIRunner.new()) + + runner.add_test_suite("res://addons/gdUnit4/test/core/discovery/resources/") + + var tests := runner.discover_tests() + assert_array(tests).has_size(12) + + +func test_discover_tests_on_file() -> void: + var runner: GdUnitTestCIRunner = auto_free(GdUnitTestCIRunner.new()) + + runner.add_test_suite("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + + var tests := runner.discover_tests() + assert_array(tests).has_size(12) + + +func test_discover_tests_on_path_and_skip_suite() -> void: + var runner: GdUnitTestCIRunner = auto_free(GdUnitTestCIRunner.new()) + + runner.add_test_suite("res://addons/gdUnit4/test/core/discovery/resources/") + runner.skip_test_suite("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + + # It will be discover two tests from DiscoverExampleTestSuite.cs + var tests := runner.discover_tests() + if ClassDB.class_exists("CSharpScript"): + assert_array(tests).has_size(2) + else: + assert_array(tests).is_empty() + + +func test_is_skipped_entire_suite_by_full_path() -> void: + var runner: GdUnitTestCIRunner = auto_free(GdUnitTestCIRunner.new()) + + var tests: Array[GdUnitTestCase] = [] + GdUnitTestDiscoverer.discover_tests(DiscoverExampleTestSuite, + func(test: GdUnitTestCase) -> void: + tests.append(test) + ) + + runner.skip_test_suite("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + + # Verify all tests are skipped + for test in tests: + assert_bool(runner.is_skipped(test)).override_failure_message("Expect '%s' is skipped" % test.test_name).is_true() + + +func test_is_skipped_entire_suite_by_name() -> void: + var runner: GdUnitTestCIRunner = auto_free(GdUnitTestCIRunner.new()) + + var tests: Array[GdUnitTestCase] = [] + GdUnitTestDiscoverer.discover_tests(DiscoverExampleTestSuite, + func(test: GdUnitTestCase) -> void: + tests.append(test) + ) + + # Skip entire suite + runner.skip_test_suite("DiscoverExampleTestSuite") + # try also skip an non existing suite + runner.skip_test_suite("NotExistingTestSuite") + + # Verify all tests are skipped + for test in tests: + assert_bool(runner.is_skipped(test)).override_failure_message("Expect '%s' is skipped" % test.test_name).is_true() + + +func test_is_skipped_single_test_by_full_path() -> void: + var runner: GdUnitTestCIRunner = auto_free(GdUnitTestCIRunner.new()) + + var tests: Array[GdUnitTestCase] = [] + GdUnitTestDiscoverer.discover_tests(DiscoverExampleTestSuite, + func(test: GdUnitTestCase) -> void: + tests.append(test) + ) + + # Skip a single test by using full path + runner.skip_test_suite("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd:test_case1") + # and by short suite name + runner.skip_test_suite("DiscoverExampleTestSuite:test_case2") + + # Verify all tests are skipped + for test in tests: + if test.test_name in ["test_case1", "test_case2"]: + assert_bool(runner.is_skipped(test)).override_failure_message("Expect '%s' is skipped" % test.test_name).is_true() + else: + assert_bool(runner.is_skipped(test)).override_failure_message("Expect '%s' is NOT skipped" % test.test_name).is_false() diff --git a/addons/gdUnit4/test/core/runners/GdUnitTestCIRunnerTest.gd.uid b/addons/gdUnit4/test/core/runners/GdUnitTestCIRunnerTest.gd.uid new file mode 100644 index 0000000..48ec5c0 --- /dev/null +++ b/addons/gdUnit4/test/core/runners/GdUnitTestCIRunnerTest.gd.uid @@ -0,0 +1 @@ +uid://cc8wba7swtffd diff --git a/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd b/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd new file mode 100644 index 0000000..80ccabd --- /dev/null +++ b/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd @@ -0,0 +1,97 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitTestSuiteTemplateTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd' + +const CUSTOM_TEMPLATE = """ + # GdUnit generated TestSuite + class_name ${suite_class_name} + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var ${source_var}_1 := ${source_class}.new() + var ${source_var}_2 = load("${source_resource_path}") +""" + + +func after() -> void: + GdUnitTestSuiteTemplate.reset_to_default(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + + +func test_default_template() -> void: + assert_str(GdUnitTestSuiteTemplate.default_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD)).is_equal(GdUnitTestSuiteTemplate.default_GD_template()) + + +func test_build_template_default() -> void: + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/script_with_class_name.gd") + var expected := """ + # GdUnit generated TestSuite + class_name ScriptWithClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + # TestSuite generated from + const __source: String = 'res://addons/gdUnit4/test/core/resources/script_with_class_name.gd' + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) + + +# checked source with class_name definition +func test_build_template_custom1() -> void: + GdUnitTestSuiteTemplate.save_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD, CUSTOM_TEMPLATE) + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/script_with_class_name.gd") + var expected := """ + # GdUnit generated TestSuite + class_name ScriptWithClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var script_with_class_name_1 := ScriptWithClassName.new() + var script_with_class_name_2 = load("res://addons/gdUnit4/test/core/resources/script_with_class_name.gd") + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) + + +# checked source without class_name definition +func test_build_template_custom2() -> void: + GdUnitTestSuiteTemplate.save_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD, CUSTOM_TEMPLATE) + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/script_without_class_name.gd") + var expected := """ + # GdUnit generated TestSuite + class_name ScriptWithoutClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var script_without_class_name_1 := ScriptWithoutClassName.new() + var script_without_class_name_2 = load("res://addons/gdUnit4/test/core/resources/script_without_class_name.gd") + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) + + +# checked source with class_name definition pascal_case +func test_build_template_custom3() -> void: + GdUnitTestSuiteTemplate.save_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD, CUSTOM_TEMPLATE) + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd") + var expected := """ + # GdUnit generated TestSuite + class_name PascalCaseWithClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var pascal_case_with_class_name_1 := PascalCaseWithClassName.new() + var pascal_case_with_class_name_2 = load("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd") + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) diff --git a/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd.uid b/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd.uid new file mode 100644 index 0000000..7db484b --- /dev/null +++ b/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd.uid @@ -0,0 +1 @@ +uid://ius5y6crbpox diff --git a/addons/gdUnit4/test/core/writers/GdUnitCsiMessageWriterTest.gd b/addons/gdUnit4/test/core/writers/GdUnitCsiMessageWriterTest.gd new file mode 100644 index 0000000..388a255 --- /dev/null +++ b/addons/gdUnit4/test/core/writers/GdUnitCsiMessageWriterTest.gd @@ -0,0 +1,30 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + + +func test_bbcode_tags_to_csi_codes_empty() -> void: + var csi_formatted := GdUnitCSIMessageWriter.new()._bbcode_tags_to_csi_codes("") + assert_str(csi_formatted).is_empty() + + +func test_bbcode_tags_to_csi_codes_color() -> void: + var bbcode:= "[color=green]line [/color][color=aqua]9:[/color] [color=#CD5C5C]Expecting:[/color]\n" + var csi_formatted := GdUnitCSIMessageWriter.new()._bbcode_tags_to_csi_codes(bbcode) + assert_str(csi_formatted)\ + .is_equal("line 9: Expecting:\n") + + +func test_bbcode_tags_to_csi_codes_color_and_bgcolor() -> void: + var bbcode:= "[color=#1E90FF]This is a[bgcolor=#00ff004d][color=white]n[/color][/bgcolor] test"\ + + " [bgcolor=#ff00004d][color=white]m[/color][/bgcolor][bgcolor=#00ff004d][color=white]M[/color][/bgcolor]essage[/color]" + var csi_formatted := GdUnitCSIMessageWriter.new()._bbcode_tags_to_csi_codes(bbcode) + assert_str(csi_formatted)\ + .is_equal("This is an test"\ + + " mM"\ + + "essage") + + +func test_bbcode_tags_to_csi_codes_text_styles() -> void: + var csi_formatted := GdUnitCSIMessageWriter.new()._bbcode_tags_to_csi_codes("This [b]is[/b] a [i]message[/i]") + assert_str(csi_formatted)\ + .is_equal("This is a message") diff --git a/addons/gdUnit4/test/core/writers/GdUnitCsiMessageWriterTest.gd.uid b/addons/gdUnit4/test/core/writers/GdUnitCsiMessageWriterTest.gd.uid new file mode 100644 index 0000000..9aee706 --- /dev/null +++ b/addons/gdUnit4/test/core/writers/GdUnitCsiMessageWriterTest.gd.uid @@ -0,0 +1 @@ +uid://dbjo0qb0veacq diff --git a/addons/gdUnit4/test/dotnet/ExampleTestSuite.cs b/addons/gdUnit4/test/dotnet/ExampleTestSuite.cs new file mode 100644 index 0000000..57a801e --- /dev/null +++ b/addons/gdUnit4/test/dotnet/ExampleTestSuite.cs @@ -0,0 +1,37 @@ +// GdUnit generated TestSuite +namespace gdUnit4.addons.gdUnit4.test.dotnet; + +#if GDUNIT4NET_API_V5 +using GdUnit4; + +using Godot; + +using static GdUnit4.Assertions; + +[TestSuite] +[RequireGodotRuntime] +public class ExampleTestSuite +{ + [TestCase] + public void IsFoo() => AssertThat("Foo").IsEqual("Foo"); + + [TestCase('A', Variant.Type.Int)] + [TestCase(sbyte.MaxValue, Variant.Type.Int)] + [TestCase(byte.MaxValue, Variant.Type.Int)] + [TestCase(short.MaxValue, Variant.Type.Int)] + [TestCase(ushort.MaxValue, Variant.Type.Int)] + [TestCase(int.MaxValue, Variant.Type.Int)] + [TestCase(uint.MaxValue, Variant.Type.Int)] + [TestCase(long.MaxValue, Variant.Type.Int)] + [TestCase(ulong.MaxValue, Variant.Type.Int)] + [TestCase(float.MaxValue, Variant.Type.Float)] + [TestCase(double.MaxValue, Variant.Type.Float)] + [TestCase("HalloWorld", Variant.Type.String)] + [TestCase(true, Variant.Type.Bool)] + public void ParameterizedTest(dynamic? value, Variant.Type type) + { + Variant v = value == null ? new Variant() : Variant.CreateFrom(value); + AssertObject(v.VariantType).IsEqual(type); + } +} +#endif diff --git a/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiLoaderTest.gd b/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiLoaderTest.gd new file mode 100644 index 0000000..153c67a --- /dev/null +++ b/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiLoaderTest.gd @@ -0,0 +1,56 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnit4CSharpApiLoaderTest +extends GdUnitTestSuite + +# TestSuite generated from +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +@warning_ignore("unused_parameter") +func before(do_skip := not GdUnit4CSharpApiLoader.is_api_loaded(), skip_reason := "Do run only for Godot .Net version") -> void: + pass + + +@warning_ignore("unused_parameter") +func test_is_engine_version_supported(version :int, expected :bool, test_parameters := [ + [0x40101, false], + [0x40102, false], + [0x40100, false], + [0x40300, true], + [0x40400, true]]) -> void: + + assert_that(GdUnit4CSharpApiLoader.is_engine_version_supported(version)).is_equal(expected) + + +func test_api_version() -> void: + assert_str(GdUnit4CSharpApiLoader.version()).starts_with("5.1.0") + + +func test_create_test_suite() -> void: + var temp := create_temp_dir("examples") + var result := GdUnitFileAccess.copy_file("res://addons/gdUnit4/test/resources/core/sources/TestPerson.cs", temp) + assert_result(result).is_success() + + var example_source_cs := result.value_as_string() + var source := load(example_source_cs) + var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, "test") + result = GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, 18, test_suite_path) + + assert_result(result).is_success() + var info: Dictionary = result.value() + assert_str(info.get("path")).is_equal("user://tmp/test/examples/TestPersonTest.cs") + assert_int(info.get("line")).is_equal(16) + + +class TestRunListener extends Node: + pass + + +func test_discover_tests() -> void: + var script: Script = load("res://addons/gdUnit4/test/dotnet/ExampleTestSuite.cs") + var tests := GdUnit4CSharpApiLoader.discover_tests(script) + + assert_array(tests).has_size(14)\ + .contains([any_class(GdUnitTestCase)]) diff --git a/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiLoaderTest.gd.uid b/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiLoaderTest.gd.uid new file mode 100644 index 0000000..d122a84 --- /dev/null +++ b/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiLoaderTest.gd.uid @@ -0,0 +1 @@ +uid://c87ltouscoxya diff --git a/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiTest.cs b/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiTest.cs new file mode 100644 index 0000000..22b7c77 --- /dev/null +++ b/addons/gdUnit4/test/dotnet/GdUnit4CSharpApiTest.cs @@ -0,0 +1,185 @@ +#if GDUNIT4NET_API_V5 +namespace gdUnit4.addons.gdUnit4.test.dotnet; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; + +using GdUnit4; + +using Godot; +using Godot.Collections; + +using src.dotnet; + +using static GdUnit4.Assertions; + +[TestSuite] +[RequireGodotRuntime] +public partial class GdUnit4CSharpApiTest +{ + [TestCase] + public void GetVersion() + { + var version = long.Parse(GdUnit4CSharpApi.Version().Replace(".", string.Empty, StringComparison.Ordinal)); + AssertThat(version).IsGreaterEqual(423); + } + + [TestCase] + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public void DiscoverTestsFromScript() + { + var script = GD.Load("res://addons/gdUnit4/test/dotnet/ExampleTestSuite.cs"); + var fullScriptPath = Path.GetFullPath(ProjectSettings.GlobalizePath(script.ResourcePath)); + var tests = GdUnit4CSharpApi.DiscoverTests(script); + + // Filter out the Guid from each test dictionary before comparing + var testsWithoutGuids = tests.Select(dict => + { + // Verify id contains the Guid and the assembly location + AssertThat(dict).ContainsKeys("guid", "assembly_location"); + AssertThat(dict["assembly_location"].AsString()).Contains("gdUnit4.dll"); + var newDict = new Dictionary(); + newDict.Merge(dict); + newDict.Remove("guid"); + newDict.Remove("assembly_location"); + return newDict; + }).ToArray(); + + AssertThat(testsWithoutGuids).HasSize(14) + // Check for single test `IsFoo` + .Contains( + new Dictionary + { + ["test_name"] = "IsFoo", + ["source_file"] = "res://addons/gdUnit4/test/dotnet/ExampleTestSuite.cs", + ["line_number"] = 16, + ["attribute_index"] = 0, + ["require_godot_runtime"] = true, + ["code_file_path"] = fullScriptPath, + ["fully_qualified_name"] = "gdUnit4.addons.gdUnit4.test.dotnet.ExampleTestSuite.IsFoo", + ["simple_name"] = "IsFoo", + ["managed_type"] = "gdUnit4.addons.gdUnit4.test.dotnet.ExampleTestSuite" + }, + // Check exemplary two of the `ParameterizedTest` (index 0, index 11) + new Dictionary + { + ["test_name"] = "ParameterizedTest", + ["source_file"] = "res://addons/gdUnit4/test/dotnet/ExampleTestSuite.cs", + ["line_number"] = 32, + ["attribute_index"] = 0, + ["require_godot_runtime"] = true, + ["code_file_path"] = fullScriptPath, + ["fully_qualified_name"] = "gdUnit4.addons.gdUnit4.test.dotnet.ExampleTestSuite.ParameterizedTest.ParameterizedTest:0 (A, 2)", + ["simple_name"] = "ParameterizedTest:0 (A, 2)", + ["managed_type"] = "gdUnit4.addons.gdUnit4.test.dotnet.ExampleTestSuite" + }, + new Dictionary + { + ["test_name"] = "ParameterizedTest", + ["source_file"] = "res://addons/gdUnit4/test/dotnet/ExampleTestSuite.cs", + ["line_number"] = 32, + ["attribute_index"] = 11, + ["require_godot_runtime"] = true, + ["code_file_path"] = fullScriptPath, + ["fully_qualified_name"] = "gdUnit4.addons.gdUnit4.test.dotnet.ExampleTestSuite.ParameterizedTest.ParameterizedTest:11 (\"HalloWorld\", 4)", + ["simple_name"] = "ParameterizedTest:11 (\"HalloWorld\", 4)", + ["managed_type"] = "gdUnit4.addons.gdUnit4.test.dotnet.ExampleTestSuite" + }); + } + + [TestCase] + public void BuildTestSuiteNodeFrom() + { + var script = GD.Load("res://addons/gdUnit4/test/dotnet/ExampleTestSuite.cs"); + var tests = GdUnit4CSharpApi.DiscoverTests(script); + + // convert the discovered tests into a test suite node + var testSuite = GdUnit4CSharpApi.BuildTestSuiteNodeFrom(tests); + AssertThat(testSuite).IsNotNull(); + AssertThat(testSuite.ManagedType).IsEqual("gdUnit4.addons.gdUnit4.test.dotnet.ExampleTestSuite"); + AssertThat(testSuite.AssemblyPath).EndsWith("gdUnit4.dll"); + AssertThat(testSuite.Tests).HasSize(14); + } + + [TestCase] + public async Task ExecuteAsync() + { + var script = GD.Load("res://addons/gdUnit4/test/dotnet/ExampleTestSuite.cs"); + var tests = GdUnit4CSharpApi.DiscoverTests(script); + + // Create a list to track received events + var receivedEvents = new List(); + + // Create a TestEventHandler object to handle the events + using var eventHandler = new TestEventHandler(); + eventHandler.EventReceived += eventData => + { + // Track the event + receivedEvents.Add( + new Dictionary + { + ["type"] = eventData["type"], + ["guid"] = eventData["guid"], + ["suite_name"] = eventData["suite_name"], + ["test_name"] = eventData["test_name"] + }); + }; + + // Create a Callable that references the handler method + var listener = new Callable(eventHandler, nameof(TestEventHandler.PublishEvent)); + + using var api = new GdUnit4CSharpApi(); + api.ExecuteAsync(tests, listener); + + // await execution is finished + await api.ToSignal(api, GdUnit4CSharpApi.SignalName.ExecutionCompleted); + + // tests * 2 (beforeTest and afterTest) + before and after + var expectedCount = (tests.Count * 2) + 2; + AssertArray(receivedEvents).HasSize(expectedCount).Contains( + new Dictionary + { + ["type"] = 2, // before + ["guid"] = "00000000-0000-0000-0000-000000000000", + ["suite_name"] = "ExampleTestSuite", + ["test_name"] = "Before" + }, + new Dictionary + { + ["type"] = 3, // after + ["guid"] = "00000000-0000-0000-0000-000000000000", + ["suite_name"] = "ExampleTestSuite", + ["test_name"] = "After" + }, + // check exemplary for one test + new Dictionary + { + ["type"] = 4, // beforeTest + ["guid"] = tests.First()["guid"], + ["suite_name"] = "ExampleTestSuite", + ["test_name"] = "IsFoo" + }, + new Dictionary + { + ["type"] = 5, // afterTest + ["guid"] = tests.First()["guid"], + ["suite_name"] = "ExampleTestSuite", + ["test_name"] = "IsFoo" + }); + } + + // Helper class to handle events + private sealed partial class TestEventHandler : RefCounted + { + // Event to notify when events are received + public event Action? EventReceived; + + // Method that will be called by the Callable + public void PublishEvent(Dictionary eventData) + => EventReceived?.Invoke(eventData); + } +} +#endif diff --git a/addons/gdUnit4/test/doubler/GdFunctionDoublerTest.gd b/addons/gdUnit4/test/doubler/GdFunctionDoublerTest.gd new file mode 100644 index 0000000..5473458 --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdFunctionDoublerTest.gd @@ -0,0 +1,20 @@ +class_name GdFunctionDoublerTest +extends GdUnitTestSuite + + +# helper to get function descriptor +func get_function_description(clazz_name: String, method_name: String) -> GdFunctionDescriptor: + var method_list := ClassDB.class_get_method_list(clazz_name) + for method_descriptor in method_list: + if method_descriptor["name"] == method_name: + return GdFunctionDescriptor.extract_from(method_descriptor) + return null + + +func get_function_description_from(clazz: Variant, method_name: String) -> GdFunctionDescriptor: + var script: GDScript = clazz + var fds := GdScriptParser.new().get_function_descriptors(script, [method_name]) + for fd in fds: + if fd.name() == method_name: + return fd + return null diff --git a/addons/gdUnit4/test/doubler/GdFunctionDoublerTest.gd.uid b/addons/gdUnit4/test/doubler/GdFunctionDoublerTest.gd.uid new file mode 100644 index 0000000..2f40b3c --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdFunctionDoublerTest.gd.uid @@ -0,0 +1 @@ +uid://bj7g4imvdh1b6 diff --git a/addons/gdUnit4/test/doubler/GdMockFunctionDoublerTest.gd b/addons/gdUnit4/test/doubler/GdMockFunctionDoublerTest.gd new file mode 100644 index 0000000..6ccfb66 --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdMockFunctionDoublerTest.gd @@ -0,0 +1,237 @@ +# GdUnit generated TestSuite +extends GdFunctionDoublerTest + + +func test_double_return_typed_function_without_arg() -> void: + var doubler := GdUnitMockFunctionDoubler.new() + # String get_class() const + var fd := get_function_description("Object", "get_class") + var expected := """ + func get_class() -> String: + var __args := [] + + if __is_prepare_return_value(): + __save_function_return_value("get_class", __args) + return "" + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("get_class", __args) + return "" + else: + __verifier.save_function_interaction("get_class", __args) + + if __is_do_not_call_real_func("get_class", __args): + return __return_mock_value("get_class", __args, "") + + return super() + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_typed_function_with_args() -> void: + var doubler := GdUnitMockFunctionDoubler.new() + # bool is_connected(signal: String, callable_: Callable)) const + var fd := get_function_description("Object", "is_connected") + var expected := """ + func is_connected(signal_, callable_) -> bool: + var __args := [signal_, callable_] + + if __is_prepare_return_value(): + __save_function_return_value("is_connected", __args) + return false + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("is_connected", __args) + return false + else: + __verifier.save_function_interaction("is_connected", __args) + + if __is_do_not_call_real_func("is_connected", __args): + return __return_mock_value("is_connected", __args, false) + + return super(signal_, callable_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_untyped_function_with_args() -> void: + var doubler := GdUnitMockFunctionDoubler.new() + + # void disconnect(signal: StringName, callable: Callable) + var fd := get_function_description("Object", "disconnect") + var expected := """ + func disconnect(signal_, callable_) -> void: + var __args := [signal_, callable_] + + if __is_prepare_return_value(): + push_error("Mocking functions with return type void is not allowed!") + return + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("disconnect", __args) + return + else: + __verifier.save_function_interaction("disconnect", __args) + + if __is_do_not_call_real_func("disconnect", __args): + return + + super(signal_, callable_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_int_function_with_varargs() -> void: + var doubler := GdUnitMockFunctionDoubler.new() + # Error emit_signal(signal: StringName, ...) vararg + var fd := get_function_description("Object", "emit_signal") + var expected := """ + func emit_signal(signal_, ...varargs_: Array) -> Error: + var __args := [signal_] + varargs_ + + if __is_prepare_return_value(): + __save_function_return_value("emit_signal", __args) + return OK + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("emit_signal", __args) + return OK + else: + __verifier.save_function_interaction("emit_signal", __args) + + if __is_do_not_call_real_func("emit_signal", __args): + return __return_mock_value("emit_signal", __args, OK) + + match varargs_.size(): + 0: return super(signal_) + 1: return super(signal_, varargs_[0]) + 2: return super(signal_, varargs_[0], varargs_[1]) + 3: return super(signal_, varargs_[0], varargs_[1], varargs_[2]) + 4: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3]) + 5: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4]) + 6: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5]) + 7: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6]) + 8: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7]) + 9: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8]) + 10: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8], varargs_[9]) + _: push_error("To many varradic arguments.") + return OK + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_untyped_function_with_varargs() -> void: + var doubler := GdUnitMockFunctionDoubler.new() + + # void emit_custom(signal_name, args__ ...) vararg const + var fd := GdFunctionDescriptor.new("emit_custom", 10, false, false, false, TYPE_NIL, "", + [GdFunctionArgument.new("signal", TYPE_SIGNAL)], + GdFunctionDescriptor._build_varargs(true)) + var expected := """ + func emit_custom(signal_, ...varargs_: Array) -> void: + var __args := [signal_] + varargs_ + + if __is_prepare_return_value(): + push_error("Mocking functions with return type void is not allowed!") + return + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("emit_custom", __args) + return + else: + __verifier.save_function_interaction("emit_custom", __args) + + if __is_do_not_call_real_func("emit_custom", __args): + return + + match varargs_.size(): + 0: super(signal_) + 1: super(signal_, varargs_[0]) + 2: super(signal_, varargs_[0], varargs_[1]) + 3: super(signal_, varargs_[0], varargs_[1], varargs_[2]) + 4: super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3]) + 5: super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4]) + 6: super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5]) + 7: super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6]) + 8: super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7]) + 9: super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8]) + 10: super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8], varargs_[9]) + _: push_error("To many varradic arguments.") + return + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_virtual_script_function_without_arg() -> void: + var doubler := GdUnitMockFunctionDoubler.new() + + # void _ready() virtual + var fd := get_function_description("Node", "_ready") + var expected := """ + func _ready() -> void: + var __args := [] + + if __is_prepare_return_value(): + push_error("Mocking functions with return type void is not allowed!") + return + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("_ready", __args) + return + else: + __verifier.save_function_interaction("_ready", __args) + + if __is_do_not_call_real_func("_ready", __args): + return + + super() + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_virtual_script_function_with_arg() -> void: + var doubler := GdUnitMockFunctionDoubler.new() + + # void _input(event: InputEvent) virtual + var fd := get_function_description("Node", "_input") + var expected := """ + func _input(event_) -> void: + var __args := [event_] + + if __is_prepare_return_value(): + push_error("Mocking functions with return type void is not allowed!") + return + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("_input", __args) + return + else: + __verifier.save_function_interaction("_input", __args) + + if __is_do_not_call_real_func("_input", __args): + return + + super(event_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) diff --git a/addons/gdUnit4/test/doubler/GdMockFunctionDoublerTest.gd.uid b/addons/gdUnit4/test/doubler/GdMockFunctionDoublerTest.gd.uid new file mode 100644 index 0000000..1d8b43f --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdMockFunctionDoublerTest.gd.uid @@ -0,0 +1 @@ +uid://dnec10t3dv4mr diff --git a/addons/gdUnit4/test/doubler/GdSpyFunctionDoublerTest.gd b/addons/gdUnit4/test/doubler/GdSpyFunctionDoublerTest.gd new file mode 100644 index 0000000..0794123 --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdSpyFunctionDoublerTest.gd @@ -0,0 +1,309 @@ +extends GdFunctionDoublerTest + + +func test_double_virtual_return_void_function_without_arg() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # void _ready() virtual + var fd := get_function_description("Node", "_ready") + var expected := """ + func _ready() -> void: + var __args := [] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("_ready", __args) + return + else: + __verifier.save_function_interaction("_ready", __args) + + super() + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_virtual_return_void_function_with_arg() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # void _input(event: InputEvent) virtual + var fd := get_function_description("Node", "_input") + var expected := """ + func _input(event_) -> void: + var __args := [event_] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("_input", __args) + return + else: + __verifier.save_function_interaction("_input", __args) + + super(event_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_typed_function_without_arg() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # String get_class() const + var fd := get_function_description("Object", "get_class") + var expected := """ + func get_class() -> String: + var __args := [] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("get_class", __args) + return "" + else: + __verifier.save_function_interaction("get_class", __args) + + return super() + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_typed_function_with_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # bool is_connected(signal_: String,Callable(target: Object,method: String)) const + var fd := get_function_description("Object", "is_connected") + var expected := """ + func is_connected(signal_, callable_) -> bool: + var __args := [signal_, callable_] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("is_connected", __args) + return false + else: + __verifier.save_function_interaction("is_connected", __args) + + return super(signal_, callable_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_void_function_with_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # void disconnect(signal_: StringName, callable_: Callable) + var fd := get_function_description("Object", "disconnect") + var expected := """ + func disconnect(signal_, callable_) -> void: + var __args := [signal_, callable_] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("disconnect", __args) + return + else: + __verifier.save_function_interaction("disconnect", __args) + + super(signal_, callable_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_void_function_without_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # void free() + var fd := get_function_description("Object", "free") + var expected := """ + func free() -> void: + var __args := [] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("free", __args) + return + else: + __verifier.save_function_interaction("free", __args) + + super() + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_typed_function_with_args_and_varargs() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # Error emit_signal(signal_: StringName, ...) vararg + var fd := get_function_description("Object", "emit_signal") + var expected := """ + func emit_signal(signal_, ...varargs_: Array) -> Error: + var __args := [signal_] + varargs_ + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("emit_signal", __args) + return OK + else: + __verifier.save_function_interaction("emit_signal", __args) + + match varargs_.size(): + 0: return super(signal_) + 1: return super(signal_, varargs_[0]) + 2: return super(signal_, varargs_[0], varargs_[1]) + 3: return super(signal_, varargs_[0], varargs_[1], varargs_[2]) + 4: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3]) + 5: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4]) + 6: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5]) + 7: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6]) + 8: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7]) + 9: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8]) + 10: return super(signal_, varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8], varargs_[9]) + _: push_error("To many varradic arguments.") + return OK + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_void_function_only_varargs() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # void bar(s...) vararg + var fd := GdFunctionDescriptor.new( "bar", 23, false, false, false, TYPE_NIL, "void", [], GdFunctionDescriptor._build_varargs(true)) + var expected := """ + func bar(...varargs_: Array) -> void: + var __args := [] + varargs_ + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("bar", __args) + return + else: + __verifier.save_function_interaction("bar", __args) + + match varargs_.size(): + 0: super() + 1: super(varargs_[0]) + 2: super(varargs_[0], varargs_[1]) + 3: super(varargs_[0], varargs_[1], varargs_[2]) + 4: super(varargs_[0], varargs_[1], varargs_[2], varargs_[3]) + 5: super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4]) + 6: super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5]) + 7: super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6]) + 8: super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7]) + 9: super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8]) + 10: super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8], varargs_[9]) + _: push_error("To many varradic arguments.") + return + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_return_typed_function_only_varargs() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # String bar(s...) vararg + var fd := GdFunctionDescriptor.new("bar", 23, false, false, false, TYPE_STRING, "String", [], GdFunctionDescriptor._build_varargs(true)) + var expected := """ + func bar(...varargs_: Array) -> String: + var __args := [] + varargs_ + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("bar", __args) + return "" + else: + __verifier.save_function_interaction("bar", __args) + + match varargs_.size(): + 0: return super() + 1: return super(varargs_[0]) + 2: return super(varargs_[0], varargs_[1]) + 3: return super(varargs_[0], varargs_[1], varargs_[2]) + 4: return super(varargs_[0], varargs_[1], varargs_[2], varargs_[3]) + 5: return super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4]) + 6: return super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5]) + 7: return super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6]) + 8: return super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7]) + 9: return super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8]) + 10: return super(varargs_[0], varargs_[1], varargs_[2], varargs_[3], varargs_[4], varargs_[5], varargs_[6], varargs_[7], varargs_[8], varargs_[9]) + _: push_error("To many varradic arguments.") + return "" + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_static_return_void_function_without_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + # void foo() + var fd := GdFunctionDescriptor.new("foo", 23, false, true, false, TYPE_NIL, "", []) + var expected := """ + static func foo() -> void: + var __args := [] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("foo", __args) + return + else: + __verifier.save_function_interaction("foo", __args) + + super() + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_static_return_void_function_with_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + var fd := GdFunctionDescriptor.new("foo", 23, false, true, false, TYPE_NIL, "", [ + GdFunctionArgument.new("arg1", TYPE_BOOL), + GdFunctionArgument.new("arg2", TYPE_STRING, "default") + ]) + var expected := """ + static func foo(arg1_, arg2_="default") -> void: + var __args := [arg1_, arg2_] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("foo", __args) + return + else: + __verifier.save_function_interaction("foo", __args) + + super(arg1_, arg2_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_static_script_function_with_args_return_bool() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + + var fd := GdFunctionDescriptor.new("foo", 23, false, true, false, TYPE_BOOL, "", [ + GdFunctionArgument.new("arg1", TYPE_BOOL), + GdFunctionArgument.new("arg2", TYPE_STRING, "default") + ]) + var expected := """ + static func foo(arg1_, arg2_="default") -> bool: + var __args := [arg1_, arg2_] + + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("foo", __args) + return false + else: + __verifier.save_function_interaction("foo", __args) + + return super(arg1_, arg2_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.double(fd))).is_equal(expected) diff --git a/addons/gdUnit4/test/doubler/GdSpyFunctionDoublerTest.gd.uid b/addons/gdUnit4/test/doubler/GdSpyFunctionDoublerTest.gd.uid new file mode 100644 index 0000000..5f125bc --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdSpyFunctionDoublerTest.gd.uid @@ -0,0 +1 @@ +uid://c4w7t2cwmogmr diff --git a/addons/gdUnit4/test/doubler/GdUnitClassDoublerTest.gd b/addons/gdUnit4/test/doubler/GdUnitClassDoublerTest.gd new file mode 100644 index 0000000..040e690 --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdUnitClassDoublerTest.gd @@ -0,0 +1,8 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + + +# simple function doubler whitout any modifications +class TestFunctionDoubler extends GdFunctionDoubler: + func double(_func_descriptor :GdFunctionDescriptor, _is_callable: bool = false) -> PackedStringArray: + return PackedStringArray([]) diff --git a/addons/gdUnit4/test/doubler/GdUnitClassDoublerTest.gd.uid b/addons/gdUnit4/test/doubler/GdUnitClassDoublerTest.gd.uid new file mode 100644 index 0000000..5ea9870 --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdUnitClassDoublerTest.gd.uid @@ -0,0 +1 @@ +uid://b64qu331wwcq diff --git a/addons/gdUnit4/test/doubler/GdUnitFunctionDoublerBuilderTest.gd b/addons/gdUnit4/test/doubler/GdUnitFunctionDoublerBuilderTest.gd new file mode 100644 index 0000000..29b2068 --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdUnitFunctionDoublerBuilderTest.gd @@ -0,0 +1,58 @@ +extends GdFunctionDoublerTest + + +func test_double_constructor_noargs() -> void: + var doubler := GdUnitSpyFunctionDoubler.new() + var fd := get_function_description("Object", "_init") + var expected := """ + func _init() -> void: + __init_doubler() + super() + """.dedent() + assert_str("\n".join(doubler.double(fd))).is_equal(expected) + + +func test_double_constructor_args() -> void: + var fd := get_function_description_from(TestClassWithConstructorArgs, "_init") + var doubler := GdUnitFunctionDoublerBuilder.new(fd) + var expected := """ + func _init(name_:="", arg1_:=0) -> void: + __init_doubler() + super(name_, arg1_) + """.dedent() + assert_str("\n".join(doubler.build())).is_equal(expected) + + +func test_double_constructor_varargs() -> void: + var fd := get_function_description_from(TestClassWithConstructorVarargs, "_init") + var doubler := GdUnitFunctionDoublerBuilder.new(fd) + var expected := """ + func _init(...varargs: Array) -> void: + __init_doubler() + super() + """.dedent() + assert_str("\n".join(doubler.build())).is_equal(expected) + + +func test_double_constructor_args_and_varargs() -> void: + var fd := get_function_description_from(TestClassWithConstructorArgsAndVarargs, "_init") + var doubler := GdUnitFunctionDoublerBuilder.new(fd) + var expected := """ + func _init(name_:="", arg_:=0, ...varargs: Array) -> void: + __init_doubler() + super(name_, arg_) + """.dedent() + assert_str("\n".join(doubler.build())).is_equal(expected) + + +func test_double_coroutine_func() -> void: + var fd := get_function_description_from(TestClassWithFunctions, "_on_test_pressed") + var doubler := GdUnitFunctionDoublerBuilder.new(fd) + var expected := """ + func _on_test_pressed(button_id_) -> void: + var __args := [button_id_] + + + await super(button_id_) + """.dedent().trim_prefix("\n") + assert_str("\n".join(doubler.build())).is_equal(expected) diff --git a/addons/gdUnit4/test/doubler/GdUnitFunctionDoublerBuilderTest.gd.uid b/addons/gdUnit4/test/doubler/GdUnitFunctionDoublerBuilderTest.gd.uid new file mode 100644 index 0000000..388048d --- /dev/null +++ b/addons/gdUnit4/test/doubler/GdUnitFunctionDoublerBuilderTest.gd.uid @@ -0,0 +1 @@ +uid://ddt4xxgelvvug diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgs.gd b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgs.gd new file mode 100644 index 0000000..1da38eb --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgs.gd @@ -0,0 +1,10 @@ +class_name TestClassWithConstructorArgs + + +@warning_ignore("unused_parameter") +func _init(name: String, arg1: int) -> void: + pass + + +func _to_string() -> String: + return "TestClassWithConstructorArgs" diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgs.gd.uid b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgs.gd.uid new file mode 100644 index 0000000..0974993 --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgs.gd.uid @@ -0,0 +1 @@ +uid://w841s45k354u diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgsAndVarargs.gd b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgsAndVarargs.gd new file mode 100644 index 0000000..b3d5e7f --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgsAndVarargs.gd @@ -0,0 +1,9 @@ +class_name TestClassWithConstructorArgsAndVarargs + +@warning_ignore("unused_parameter") +func _init(name: String, arg: int, ...args: Array) -> void: + pass + + +func _to_string() -> String: + return "TestClassWithConstructorArgsAndVarargs" diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgsAndVarargs.gd.uid b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgsAndVarargs.gd.uid new file mode 100644 index 0000000..7bbb7bf --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorArgsAndVarargs.gd.uid @@ -0,0 +1 @@ +uid://drpfneh0oct15 diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorVarargs.gd b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorVarargs.gd new file mode 100644 index 0000000..b2765ff --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorVarargs.gd @@ -0,0 +1,9 @@ +class_name TestClassWithConstructorVarargs + +@warning_ignore("unused_parameter") +func _init(...args: Array) -> void: + pass + + +func _to_string() -> String: + return "TestClassWithConstructorVarargs" diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorVarargs.gd.uid b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorVarargs.gd.uid new file mode 100644 index 0000000..a1db21a --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithConstructorVarargs.gd.uid @@ -0,0 +1 @@ +uid://qcp6pfe3uii6 diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithFunctions.gd b/addons/gdUnit4/test/doubler/resources/TestClassWithFunctions.gd new file mode 100644 index 0000000..e5b4638 --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithFunctions.gd @@ -0,0 +1,21 @@ +class_name TestClassWithFunctions +extends Control + +signal panel_color_change(box :ColorRect, color :Color) + +@onready var _box1 := $VBoxContainer/PanelContainer/HBoxContainer/Panel1 +@onready var _box2 := $VBoxContainer/PanelContainer/HBoxContainer/Panel2 +@onready var _box3 := $VBoxContainer/PanelContainer/HBoxContainer/Panel3 + + +func _on_test_pressed(button_id: int) -> void: + var box: ColorRect + match button_id: + 1: box = _box1 + 2: box = _box2 + 3: box = _box3 + panel_color_change.emit(box, Color.RED) + # special case for button 3 we wait 1s to change to gray + if button_id == 3: + await get_tree().create_timer(1).timeout + panel_color_change.emit(box, Color.GRAY) diff --git a/addons/gdUnit4/test/doubler/resources/TestClassWithFunctions.gd.uid b/addons/gdUnit4/test/doubler/resources/TestClassWithFunctions.gd.uid new file mode 100644 index 0000000..ed003e9 --- /dev/null +++ b/addons/gdUnit4/test/doubler/resources/TestClassWithFunctions.gd.uid @@ -0,0 +1 @@ +uid://0d8s3gna0idg diff --git a/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd b/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd new file mode 100644 index 0000000..3ae397d --- /dev/null +++ b/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd @@ -0,0 +1,66 @@ +# GdUnit generated TestSuite +class_name GdUnitFuncValueExtractorTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd' + + +class TestNode extends Resource: + var _parent :TestNode = null + var _children := Array() + + func _init(name :String, parent :TestNode = null) -> void: + set_name(name) + _parent = parent + if _parent: + _parent._children.append(self) + + + func _notification(what :int) -> void: + if what == NOTIFICATION_PREDELETE: + _parent = null + _children.clear() + + + func get_parent() -> TestNode: + return _parent + + + func get_children() -> Array: + return _children + + + +func test_extract_value_success() -> void: + var node :TestNode = auto_free(TestNode.new("node_a")) + + assert_str(GdUnitFuncValueExtractor.new("get_name", []).extract_value(node)).is_equal("node_a") + + +func test_extract_value_func_not_exists() -> void: + var node :TestNode = TestNode.new("node_a") + + assert_str(GdUnitFuncValueExtractor.new("get_foo", []).extract_value(node)).is_equal("n.a.") + + +func test_extract_value_on_null_value() -> void: + assert_str(GdUnitFuncValueExtractor.new("get_foo", []).extract_value(null)).is_null() + + +func test_extract_value_chanined() -> void: + var parent :TestNode = TestNode.new("parent") + var node :TestNode = auto_free(TestNode.new("node_a", parent)) + + assert_str(GdUnitFuncValueExtractor.new("get_name", []).extract_value(node)).is_equal("node_a") + assert_str(GdUnitFuncValueExtractor.new("get_parent.get_name", []).extract_value(node)).is_equal("parent") + + +func test_extract_value_chanined_array_values() -> void: + var parent :TestNode = TestNode.new("parent") + auto_free(TestNode.new("node_a", parent)) + auto_free(TestNode.new("node_b", parent)) + auto_free(TestNode.new("node_c", parent)) + + assert_array(GdUnitFuncValueExtractor.new("get_children.get_name", []).extract_value(parent))\ + .contains_exactly(["node_a", "node_b", "node_c"]) diff --git a/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd.uid b/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd.uid new file mode 100644 index 0000000..1010522 --- /dev/null +++ b/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd.uid @@ -0,0 +1 @@ +uid://c3e5jplnbk7ae diff --git a/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd b/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd new file mode 100644 index 0000000..83a4609 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd @@ -0,0 +1,162 @@ +extends GdUnitTestSuite + +var _current_iterations : Dictionary +var _expected_iterations: Dictionary + +# a simple test fuzzer where provided a hard coded value set +class TestFuzzer extends Fuzzer: + var _data := [0, 1, 2, 3, 4, 5, 6, 23, 8, 9] + + + func next_value() -> int: + return _data.pop_front() + + +func max_value() -> int: + return 10 + + +func min_value() -> int: + return 1 + + +func get_fuzzer() -> Fuzzer: + return Fuzzers.rangei(min_value(), max_value()) + + +func before() -> void: + # define expected iteration count + _expected_iterations = { + "test_fuzzer_has_same_instance_peer_iteration" : 10, + "test_multiple_fuzzers_inject_value_with_seed" : 10, + "test_fuzzer_iterations_default" : Fuzzer.ITERATION_DEFAULT_COUNT, + "test_fuzzer_iterations_custom_value" : 234, + "test_fuzzer_inject_value" : 100, + "test_multiline_fuzzer_args": 23, + } + # inital values + _current_iterations = { + "test_fuzzer_has_same_instance_peer_iteration" : 0, + "test_multiple_fuzzers_inject_value_with_seed" : 0, + "test_fuzzer_iterations_default" : 0, + "test_fuzzer_iterations_custom_value" : 0, + "test_fuzzer_inject_value" : 0, + "test_multiline_fuzzer_args": 0, + } + + +func after() -> void: + for test_case :String in _expected_iterations.keys(): + var current :int = _current_iterations[test_case] + var expected :int = _expected_iterations[test_case] + + assert_int(current).override_failure_message("Expecting %s itertions but is %s checked test case %s" % [expected, current, test_case]).is_equal(expected) + +var _fuzzer_instance_before : Fuzzer = null + + +func test_fuzzer_has_same_instance_peer_iteration(fuzzer := TestFuzzer.new(), _fuzzer_iterations := 10) -> void: + _current_iterations["test_fuzzer_has_same_instance_peer_iteration"] += 1 + assert_object(fuzzer).is_not_null() + if _fuzzer_instance_before != null: + assert_object(fuzzer).is_same(_fuzzer_instance_before) + _fuzzer_instance_before = fuzzer + + +func test_fuzzer_iterations_default(fuzzer := Fuzzers.rangei(-23, 22)) -> void: + _current_iterations["test_fuzzer_iterations_default"] += 1 + assert_object(fuzzer).is_not_null() + assert_int(fuzzer.next_value()).is_between(-23, 22) + + +func test_fuzzer_iterations_custom_value(_fuzzer := Fuzzers.rangei(-23, 22), _fuzzer_iterations := 234, _fuzzer_seed := 100) -> void: + _current_iterations["test_fuzzer_iterations_custom_value"] += 1 + + +func test_fuzzer_inject_value(fuzzer := Fuzzers.rangei(-23, 22), _fuzzer_iterations := 100) -> void: + _current_iterations["test_fuzzer_inject_value"] += 1 + assert_object(fuzzer).is_not_null() + assert_int(fuzzer.next_value()).is_between(-23, 22) + + +func test_fuzzer_with_timeout(fuzzer := Fuzzers.rangei(-23, 22), _fuzzer_iterations := 20, _timeout := 100) -> void: + discard_error_interupted_by_timeout() + assert_int(fuzzer.next_value()).is_between(-23, 22) + + if fuzzer.iteration_index() == 10: + await await_millis(100) + # we not expect more than 10 iterations it should be interuptead by a timeout + assert_int(fuzzer.iteration_index()).is_less_equal(10) + +var expected_value: Array[int] = [22, 3, -14, -16, 21, 20, 4, -23, -19, -5] + + +func test_fuzzer_inject_value_with_seed(fuzzer := Fuzzers.rangei(-23, 22), _fuzzer_iterations := 10, _fuzzer_seed := 187772) -> void: + assert_object(fuzzer).is_not_null() + var iteration_index := fuzzer.iteration_index()-1 + var current := fuzzer.next_value() + var expected := expected_value[iteration_index] + assert_int(iteration_index).is_between(0, 9).is_less(10) + assert_int(current)\ + .override_failure_message("Expect value %s checked test iteration %s\n but was %s" % [expected, iteration_index, current])\ + .is_equal(expected) + +var expected_value_a: Array[int] = [22, -14, 21, 4, -19, -11, 5, 21, -6, -9] +var expected_value_b: Array[int] = [35, 38, 34, 39, 35, 41, 37, 35, 34, 39] + + +func test_multiple_fuzzers_inject_value_with_seed(fuzzer_a := Fuzzers.rangei(-23, 22), fuzzer_b := Fuzzers.rangei(33, 44), _fuzzer_iterations := 10, _fuzzer_seed := 187772) -> void: + _current_iterations["test_multiple_fuzzers_inject_value_with_seed"] += 1 + assert_object(fuzzer_a).is_not_null() + assert_object(fuzzer_b).is_not_null() + var iteration_index_a := fuzzer_a.iteration_index()-1 + var current_a := fuzzer_a.next_value() + var expected_a := expected_value_a[iteration_index_a] + assert_int(iteration_index_a).is_between(0, 9).is_less(10) + assert_int(current_a).is_between(-23, 22) + assert_int(current_a)\ + .override_failure_message("Expect value %s checked test iteration %s\n but was %s" % [expected_a, iteration_index_a, current_a])\ + .is_equal(expected_a) + var iteration_index_b := fuzzer_b.iteration_index()-1 + var current_b := fuzzer_b.next_value() + var expected_b := expected_value_b[iteration_index_b] + assert_int(iteration_index_b).is_between(0, 9).is_less(10) + assert_int(current_b).is_between(33, 44) + assert_int(current_b)\ + .override_failure_message("Expect value %s checked test iteration %s\n but was %s" % [expected_b, iteration_index_b, current_b])\ + .is_equal(expected_b) + + +func test_fuzzer_error_after_eight_iterations(fuzzer:=TestFuzzer.new(), _fuzzer_iterations := 10) -> void: + assert_object(fuzzer).is_not_null() + # should fail after 8 iterations + if fuzzer.iteration_index() == 8: + assert_failure(func() -> void: assert_int(fuzzer.next_value()).is_between(0, 9)) \ + .is_failed() \ + .has_message("Expecting:\n '23'\n in range between\n '0' <> '9'") + else: + assert_int(fuzzer.next_value()).is_between(0, 9) + + +func test_fuzzer_custom_func(fuzzer := get_fuzzer()) -> void: + assert_object(fuzzer).is_not_null() + assert_int(fuzzer.next_value()).is_between(1, 10) + + +func test_multiline_fuzzer_args( + fuzzer_a := Fuzzers.rangev2(Vector2(-47, -47), Vector2(47, 47)), + fuzzer_b := Fuzzers.rangei(0, 9), + _fuzzer_iterations := 23) -> void: + assert_object(fuzzer_a).is_not_null() + assert_object(fuzzer_b).is_not_null() + _current_iterations["test_multiline_fuzzer_args"] += 1 + + +@warning_ignore("untyped_declaration") +func test_fuzzing_with_untyped_parameters(float_fuzzer = Fuzzers.rangef(-100.0, 100.0), _fuzzer_iterations = 10): + assert_float(float_fuzzer.next_value()).is_between(-100.0, 100.0) + + +func test_boolean_fuzzer(bool_fuzzer := Fuzzers.boolean(), _fuzzer_iterations := 50) -> void: + var value := bool_fuzzer.next_value() + assert_bool(value in [true, false]).is_true() diff --git a/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd.uid b/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd.uid new file mode 100644 index 0000000..e568167 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd.uid @@ -0,0 +1 @@ +uid://cylhj1f4n0gsu diff --git a/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd b/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd new file mode 100644 index 0000000..0c6ad50 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd @@ -0,0 +1,66 @@ +# GdUnit generated TestSuite +class_name StringFuzzerTest +extends GdUnitTestSuite + + +func test_extract_charset() -> void: + assert_str(StringFuzzer._extract_charset("abc").to_byte_array().get_string_from_utf32()).is_equal("abc") + assert_str(StringFuzzer._extract_charset("abcDXG").to_byte_array().get_string_from_utf32()).is_equal("abcDXG") + assert_str(StringFuzzer._extract_charset("a-c").to_byte_array().get_string_from_utf32()).is_equal("abc") + assert_str(StringFuzzer._extract_charset("a-z").to_byte_array().get_string_from_utf32()).is_equal("abcdefghijklmnopqrstuvwxyz") + assert_str(StringFuzzer._extract_charset("A-Z").to_byte_array().get_string_from_utf32()).is_equal("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + # unicode + assert_str(StringFuzzer._extract_charset("abcๆ—ฅๆœฌ่ชžDXG").to_byte_array().get_string_from_utf32()).is_equal("abcๆ—ฅๆœฌ่ชžDXG") + + # range token at start + assert_str(StringFuzzer._extract_charset("-a-dA-D2-8+_").to_byte_array().get_string_from_utf32()).is_equal("-abcdABCD2345678+_") + # range token at end + assert_str(StringFuzzer._extract_charset("a-dA-D2-8+_-").to_byte_array().get_string_from_utf32()).is_equal("abcdABCD2345678+_-") + # range token in the middle + assert_str(StringFuzzer._extract_charset("a-d-A-D2-8+_").to_byte_array().get_string_from_utf32()).is_equal("abcd-ABCD2345678+_") + + +func test_next_value() -> void: + var pattern := "a-cD-X+2-5" + var fuzzer := StringFuzzer.new(4, 128, pattern) + var r := RegEx.new() + r.compile("[%s]+" % pattern) + for i in 100: + var value := fuzzer.next_value() + # verify the generated value has a length in the configured min/max range + assert_int(value.length()).is_between(4, 128) + # using regex to remove_at all expected chars to verify the value only containing expected chars by is empty + assert_str(r.sub(value, "")).is_empty() + + +func test_next_value_min_max_boundaries() -> void: + var boundaries := {} + var fuzzer := StringFuzzer.new(2, 3, "A") + for i in 200: + var value := fuzzer.next_value() + boundaries[value.length()] = boundaries.get_or_add(value.length(), 0) + + # verify it contains only values with length 2 or 3 + assert_dict(boundaries)\ + .is_not_empty()\ + .has_size(2)\ + .contains_keys(2, 3) + + +func test_next_value_min_max_same_boundaries() -> void: + var boundaries := {} + var fuzzer := StringFuzzer.new(2, 2, "A") + for i in 200: + var value :String = fuzzer.next_value() + boundaries[value.length()] = boundaries.get_or_add(value.length(), 0) + + # verify it contains only values with length 2 or 3 + assert_dict(boundaries)\ + .is_not_empty()\ + .has_size(1)\ + .contains_keys(2) + + +func test_password(fuzzer := StringFuzzer.new(8, 32, "a-zA-Z0-9!@#$%"), _fuzzer_iterations := 100) -> void: + var password := fuzzer.next_value() + assert_str(password).has_length(8, Comparator.GREATER_EQUAL).has_length(32, Comparator.LESS_EQUAL) diff --git a/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd.uid b/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd.uid new file mode 100644 index 0000000..3239c0b --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd.uid @@ -0,0 +1 @@ +uid://dgj06ja5nhp68 diff --git a/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd b/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd new file mode 100644 index 0000000..083ed73 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd @@ -0,0 +1,10 @@ +class_name TestExternalFuzzer +extends Fuzzer + + +func _init() -> void: + pass + + +func next_value() -> Variant: + return {} diff --git a/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd.uid b/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd.uid new file mode 100644 index 0000000..eb25af2 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd.uid @@ -0,0 +1 @@ +uid://dx30ieo0y68i5 diff --git a/addons/gdUnit4/test/fuzzers/TestFuzzers.gd b/addons/gdUnit4/test/fuzzers/TestFuzzers.gd new file mode 100644 index 0000000..dbb5e59 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/TestFuzzers.gd @@ -0,0 +1,38 @@ +extends RefCounted + +const MIN_VALUE := -10 +const MAX_VALUE := 22 + +class NestedFuzzer extends Fuzzer: + + func _init() -> void: + pass + + func next_value() -> Variant: + return {} + + static func _s_max_value() -> int: + return MAX_VALUE + + +class NestedFuzzerWithArgs extends Fuzzer: + + var _value: Variant + + func _init(value: int, _max_value := MAX_VALUE, _vec := Vector2.ONE) -> void: + _value = value + + func next_value() -> Variant: + return _value + + +func min_value() -> int: + return MIN_VALUE + + +func get_fuzzer() -> Fuzzer: + return Fuzzers.rangei(min_value(), NestedFuzzer._s_max_value()) + + +func non_fuzzer() -> Resource: + return Image.new() diff --git a/addons/gdUnit4/test/fuzzers/TestFuzzers.gd.uid b/addons/gdUnit4/test/fuzzers/TestFuzzers.gd.uid new file mode 100644 index 0000000..19d440a --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/TestFuzzers.gd.uid @@ -0,0 +1 @@ +uid://d13soy86k6djt diff --git a/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd new file mode 100644 index 0000000..54235dd --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd @@ -0,0 +1,29 @@ +# GdUnit generated TestSuite +class_name AnyArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd' + + +func test_is_match() -> void: + var matcher := AnyArgumentMatcher.new() + + assert_bool(matcher.is_match(null)).is_true() + assert_bool(matcher.is_match("")).is_true() + assert_bool(matcher.is_match("abc")).is_true() + assert_bool(matcher.is_match(true)).is_true() + assert_bool(matcher.is_match(false)).is_true() + assert_bool(matcher.is_match(0)).is_true() + assert_bool(matcher.is_match(100010)).is_true() + assert_bool(matcher.is_match(1.2)).is_true() + assert_bool(matcher.is_match(RefCounted.new())).is_true() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_true() + + +func test_any() -> void: + assert_object(any()).is_instanceof(AnyArgumentMatcher) + + +func test_to_string() -> void: + assert_str(str(any())).is_equal("any()") diff --git a/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd.uid b/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd.uid new file mode 100644 index 0000000..aa5b867 --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd.uid @@ -0,0 +1 @@ +uid://bnuavtxwq2wj8 diff --git a/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd new file mode 100644 index 0000000..a5448f5 --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd @@ -0,0 +1,228 @@ +# GdUnit generated TestSuite +class_name AnyBuildInTypeArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd' + + +func test_is_match_bool() -> void: + assert_object(any_bool()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_bool() + assert_bool(matcher.is_match(true)).is_true() + assert_bool(matcher.is_match(false)).is_true() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(0.2)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_int() -> void: + assert_object(any_int()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_int() + assert_bool(matcher.is_match(0)).is_true() + assert_bool(matcher.is_match(1000)).is_true() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match([])).is_false() + assert_bool(matcher.is_match(0.2)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_float() -> void: + assert_object(any_float()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_float() + assert_bool(matcher.is_match(.0)).is_true() + assert_bool(matcher.is_match(0.0)).is_true() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match([])).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_string() -> void: + assert_object(any_string()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_string() + assert_bool(matcher.is_match("")).is_true() + assert_bool(matcher.is_match("abc")).is_true() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match([])).is_false() + assert_bool(matcher.is_match(0.2)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_color() -> void: + assert_object(any_color()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_color() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Color.ALICE_BLUE)).is_true() + assert_bool(matcher.is_match(Color.RED)).is_true() + + +func test_is_match_vector() -> void: + assert_object(any_vector()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector() + assert_bool(matcher.is_match(Vector2.ONE)).is_true() + assert_bool(matcher.is_match(Vector2i.ONE)).is_true() + assert_bool(matcher.is_match(Vector3.ONE)).is_true() + assert_bool(matcher.is_match(Vector3i.ONE)).is_true() + assert_bool(matcher.is_match(Vector4.ONE)).is_true() + assert_bool(matcher.is_match(Vector4i.ONE)).is_true() + + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + + +func test_is_match_vector2() -> void: + assert_object(any_vector2()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector2() + assert_bool(matcher.is_match(Vector2.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector2i() -> void: + assert_object(any_vector2i()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector2i() + assert_bool(matcher.is_match(Vector2i.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector3() -> void: + assert_object(any_vector3()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector3() + assert_bool(matcher.is_match(Vector3.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector3i() -> void: + assert_object(any_vector3i()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector3i() + assert_bool(matcher.is_match(Vector3i.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector4() -> void: + assert_object(any_vector4()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector4() + assert_bool(matcher.is_match(Vector4.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector4i() -> void: + assert_object(any_vector4i()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector4i() + assert_bool(matcher.is_match(Vector4i.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + + +func test_to_string() -> void: + assert_str(str(any_bool())).is_equal("any_bool()") + assert_str(str(any_int())).is_equal("any_int()") + assert_str(str(any_float())).is_equal("any_float()") + assert_str(str(any_string())).is_equal("any_string()") + assert_str(str(any_color())).is_equal("any_color()") + assert_str(str(any_vector())).is_equal("any_vector()") + assert_str(str(any_vector2())).is_equal("any_vector2()") + assert_str(str(any_vector2i())).is_equal("any_vector2i()") + assert_str(str(any_vector3())).is_equal("any_vector3()") + assert_str(str(any_vector3i())).is_equal("any_vector3i()") + assert_str(str(any_vector4())).is_equal("any_vector4()") + assert_str(str(any_vector4i())).is_equal("any_vector4i()") + assert_str(str(any_rect2())).is_equal("any_rect2()") + assert_str(str(any_plane())).is_equal("any_plane()") + assert_str(str(any_quat())).is_equal("any_quat()") + assert_str(str(any_quat())).is_equal("any_quat()") + assert_str(str(any_basis())).is_equal("any_basis()") + assert_str(str(any_transform_2d())).is_equal("any_transform_2d()") + assert_str(str(any_transform_3d())).is_equal("any_transform_3d()") + assert_str(str(any_node_path())).is_equal("any_node_path()") + assert_str(str(any_rid())).is_equal("any_rid()") + assert_str(str(any_object())).is_equal("any_object()") + assert_str(str(any_dictionary())).is_equal("any_dictionary()") + assert_str(str(any_array())).is_equal("any_array()") + assert_str(str(any_packed_byte_array())).is_equal("any_packed_byte_array()") + assert_str(str(any_packed_int32_array())).is_equal("any_packed_int32_array()") + assert_str(str(any_packed_int64_array())).is_equal("any_packed_int64_array()") + assert_str(str(any_packed_float32_array())).is_equal("any_packed_float32_array()") + assert_str(str(any_packed_float64_array())).is_equal("any_packed_float64_array()") + assert_str(str(any_packed_string_array())).is_equal("any_packed_string_array()") + assert_str(str(any_packed_vector2_array())).is_equal("any_packed_vector2_array()") + assert_str(str(any_packed_vector3_array())).is_equal("any_packed_vector3_array()") + assert_str(str(any_packed_color_array())).is_equal("any_packed_color_array()") diff --git a/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd.uid b/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd.uid new file mode 100644 index 0000000..427f842 --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd.uid @@ -0,0 +1 @@ +uid://cqs7esj70f6xf diff --git a/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd new file mode 100644 index 0000000..49cc7b2 --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd @@ -0,0 +1,43 @@ +# GdUnit generated TestSuite +class_name AnyClazzArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd' + + +func test_is_match_reference() -> void: + var matcher := AnyClazzArgumentMatcher.new(RefCounted) + + assert_bool(matcher.is_match(Resource.new())).is_true() + assert_bool(matcher.is_match(RefCounted.new())).is_true() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(false)).is_false() + assert_bool(matcher.is_match(true)).is_false() + + +func test_is_match_node() -> void: + var matcher := AnyClazzArgumentMatcher.new(Node) + + assert_bool(matcher.is_match(auto_free(Node.new()))).is_true() + assert_bool(matcher.is_match(auto_free(AnimationPlayer.new()))).is_true() + assert_bool(matcher.is_match(auto_free(Timer.new()))).is_true() + assert_bool(matcher.is_match(Resource.new())).is_false() + assert_bool(matcher.is_match(RefCounted.new())).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(false)).is_false() + assert_bool(matcher.is_match(true)).is_false() + + +func test_any_class() -> void: + assert_object(any_class(Node)).is_instanceof(AnyClazzArgumentMatcher) + + +func test_to_string() -> void: + assert_str(str(any_class(Node))).is_equal("any_class()") + assert_str(str(any_class(Object))).is_equal("any_class()") + assert_str(str(any_class(RefCounted))).is_equal("any_class()") + assert_str(str(any_class(GdObjects))).is_equal("any_class()") diff --git a/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd.uid b/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd.uid new file mode 100644 index 0000000..06e197e --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd.uid @@ -0,0 +1 @@ +uid://bgq1dye4qwl6v diff --git a/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd new file mode 100644 index 0000000..93abb8b --- /dev/null +++ b/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd @@ -0,0 +1,36 @@ +# GdUnit generated TestSuite +class_name ChainedArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd' + + +func test_is_match_one_arg() -> void: + var matchers := [ + EqualsArgumentMatcher.new("foo") + ] + var matcher := ChainedArgumentMatcher.new(matchers) + + assert_bool(matcher.is_match(["foo"])).is_true() + assert_bool(matcher.is_match(["bar"])).is_false() + + +func test_is_match_two_arg() -> void: + var matchers := [ + EqualsArgumentMatcher.new("foo"), + EqualsArgumentMatcher.new("value1") + ] + var matcher := ChainedArgumentMatcher.new(matchers) + + assert_bool(matcher.is_match(["foo", "value1"])).is_true() + assert_bool(matcher.is_match(["foo", "value2"])).is_false() + assert_bool(matcher.is_match(["bar", "value1"])).is_false() + + +func test_is_match_different_arg_and_matcher() -> void: + var matchers := [ + EqualsArgumentMatcher.new("foo") + ] + var matcher := ChainedArgumentMatcher.new(matchers) + assert_bool(matcher.is_match(["foo", "value"])).is_false() diff --git a/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd.uid b/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd.uid new file mode 100644 index 0000000..188dad0 --- /dev/null +++ b/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd.uid @@ -0,0 +1 @@ +uid://dailfe18ih5ic diff --git a/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd new file mode 100644 index 0000000..840cda6 --- /dev/null +++ b/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd @@ -0,0 +1,26 @@ +@warning_ignore_start("unsafe_method_access") +extends GdUnitTestSuite + + +class CustomArgumentMatcher extends GdUnitArgumentMatcher: + var _peek :int + + func _init(peek :int) -> void: + _peek = peek + + func is_match(value :Variant) -> bool: + return value > _peek + + +func test_custom_matcher() -> void: + var mocked_test_class : CustomArgumentMatcherTestClass = mock(CustomArgumentMatcherTestClass) + + mocked_test_class.set_value(1000) + mocked_test_class.set_value(1001) + mocked_test_class.set_value(1002) + mocked_test_class.set_value(2002) + + # counts 1001, 1002, 2002 = 3 times + verify(mocked_test_class, 3).set_value(CustomArgumentMatcher.new(1000)) + # counts 2002 = 1 times + verify(mocked_test_class, 1).set_value(CustomArgumentMatcher.new(2000)) diff --git a/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd.uid b/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd.uid new file mode 100644 index 0000000..1654a3b --- /dev/null +++ b/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd.uid @@ -0,0 +1 @@ +uid://dluww6jhj5u12 diff --git a/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd b/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd new file mode 100644 index 0000000..1053f37 --- /dev/null +++ b/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd @@ -0,0 +1,16 @@ +# GdUnit generated TestSuite +class_name GdUnitArgumentMatchersTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd' + + +func test_arguments_to_chained_matcher() -> void: + var matcher := GdUnitArgumentMatchers.to_matcher(["foo", false, 1]) + + assert_object(matcher).is_instanceof(ChainedArgumentMatcher) + assert_bool(matcher.is_match(["foo", false, 1])).is_true() + assert_bool(matcher.is_match(["foo", false, 2])).is_false() + assert_bool(matcher.is_match(["foo", true, 1])).is_false() + assert_bool(matcher.is_match(["bar", false, 1])).is_false() diff --git a/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd.uid b/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd.uid new file mode 100644 index 0000000..6dc2898 --- /dev/null +++ b/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd.uid @@ -0,0 +1 @@ +uid://stj3akec2n8o diff --git a/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd b/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd new file mode 100644 index 0000000..a0d6d72 --- /dev/null +++ b/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd @@ -0,0 +1,8 @@ +class_name CustomArgumentMatcherTestClass +extends RefCounted + +var _value :int + + +func set_value(value :int) -> void: + _value = value diff --git a/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd.uid b/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd.uid new file mode 100644 index 0000000..28b2c39 --- /dev/null +++ b/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd.uid @@ -0,0 +1 @@ +uid://tjmy5q8reoem diff --git a/addons/gdUnit4/test/mocker/CustomEnums.gd b/addons/gdUnit4/test/mocker/CustomEnums.gd new file mode 100644 index 0000000..0ed0f91 --- /dev/null +++ b/addons/gdUnit4/test/mocker/CustomEnums.gd @@ -0,0 +1,8 @@ +class_name CustomEnums +extends RefCounted + + +enum TEST_ENUM { + FOO = 11, + BAR = 22 +} diff --git a/addons/gdUnit4/test/mocker/CustomEnums.gd.uid b/addons/gdUnit4/test/mocker/CustomEnums.gd.uid new file mode 100644 index 0000000..54a2efa --- /dev/null +++ b/addons/gdUnit4/test/mocker/CustomEnums.gd.uid @@ -0,0 +1 @@ +uid://djxtlu8fmgt7x diff --git a/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd b/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd new file mode 100644 index 0000000..33b8714 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd @@ -0,0 +1,50 @@ +# GdUnit generated TestSuite +class_name GdUnitMockBuilderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd' + +func test_mock_on_script_path_without_class_name() -> void: + var instance :Object = (load("res://addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd") as GDScript).new() + var script := GdUnitMockBuilder.mock_on_script(instance, "res://addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd", [], true); + assert_str(script.resource_name).starts_with("MockClassWithoutNameA") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) + + +func test_mock_on_script_path_with_custom_class_name() -> void: + # the class contains a class_name definition + var instance :Object = (load("res://addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd") as GDScript).new() + var script := GdUnitMockBuilder.mock_on_script(instance, "res://addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd", [], false); + assert_str(script.resource_name).starts_with("MockGdUnitTestCustomClassName") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) + + +func test_mock_on_class_with_class_name() -> void: + var script := GdUnitMockBuilder.mock_on_script(ClassWithNameA.new(), ClassWithNameA, [], false); + assert_str(script.resource_name).starts_with("MockClassWithNameA") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) + + +func test_mock_on_class_with_custom_class_name() -> void: + # the class contains a class_name definition + var script := GdUnitMockBuilder.mock_on_script(GdUnit_Test_CustomClassName.new(), GdUnit_Test_CustomClassName, [], false); + assert_str(script.resource_name).starts_with("MockGdUnitTestCustomClassName") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) + + +func test_mock_on_script_with_multilines() -> void: + var instance :Object = (load("res://addons/gdUnit4/test/mocker/resources/ClassWithMultilineBlocks.gd") as GDScript).new() + var script := GdUnitMockBuilder.mock_on_script(instance, instance.get_script(), [], false); + assert_str(script.resource_name).starts_with("MockClassWithMultilineBlocks") + assert_that(script.get_instance_base_type()).is_equal("RefCounted") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) diff --git a/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd.uid b/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd.uid new file mode 100644 index 0000000..1092510 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd.uid @@ -0,0 +1 @@ +uid://c4x26hsb36uym diff --git a/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd b/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd new file mode 100644 index 0000000..13d5f97 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd @@ -0,0 +1,1192 @@ +@warning_ignore_start("unsafe_method_access") +class_name GdUnitMockerTest +extends GdUnitTestSuite + + +var resource_path := "res://addons/gdUnit4/test/mocker/resources/" + +func before() -> void: + # disable error pushing for testing + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + + +func test_mock_instance_id_is_unique() -> void: + var m1: Variant = mock(RefCounted) + var m2: Variant = mock(RefCounted) + # test the internal instance id is unique + assert_object(m1).is_not_same(m2) + + +func test_is_mockable_godot_classes() -> void: + # verify enigne classes + for clazz_name in ClassDB.get_class_list(): + # mocking is not allowed for: + # singleton classes + # unregistered classes in ClassDB + # protected classes (name starts with underscore) + var is_mockable :bool = not Engine.has_singleton(clazz_name) and ClassDB.can_instantiate(clazz_name) and clazz_name.find("_") != 0 + assert_bool(GdUnitMockBuilder.is_mockable(clazz_name)) \ + .override_failure_message("Class '%s' expect mockable %s" % [clazz_name, is_mockable]) \ + .is_equal(is_mockable) + + +func test_is_mockable_by_class_type() -> void: + assert_bool(GdUnitMockBuilder.is_mockable(Node)).is_true() + assert_bool(GdUnitMockBuilder.is_mockable(CSGBox3D)).is_true() + + +func test_is_mockable_custom_class_type() -> void: + assert_bool(GdUnitMockBuilder.is_mockable(CustomResourceTestClass)).is_true() + assert_bool(GdUnitMockBuilder.is_mockable(CustomNodeTestClass)).is_true() + + +func test_is_mockable_by_script_path() -> void: + assert_bool(GdUnitMockBuilder.is_mockable(resource_path + "CustomResourceTestClass.gd")).is_true() + assert_bool(GdUnitMockBuilder.is_mockable(resource_path + "CustomNodeTestClass.gd")).is_true() + # verify for non scripts + assert_bool(GdUnitMockBuilder.is_mockable(resource_path + "capsuleshape2d.tres")).is_false() + + +func test_is_mockable__overriden_func_get_class() -> void: + # test with class type + assert_bool(GdUnitMockBuilder.is_mockable(OverridenGetClassTestClass))\ + .override_failure_message("The class 'CustomResourceTestClass' should be mockable when 'func get_class()' is overriden")\ + .is_true() + # test with resource path + assert_bool(GdUnitMockBuilder.is_mockable(resource_path + "OverridenGetClassTestClass.gd"))\ + .override_failure_message("The class 'CustomResourceTestClass' should be mockable when 'func get_class()' is overriden")\ + .is_true() + + +func test_mock_godot_class_fullcheck(fuzzer := GodotClassNameFuzzer.new(), _fuzzer_iterations := 200) -> void: + var clazz_name := fuzzer.next_value() + # try to create a mock + if GdUnitMockBuilder.is_mockable(clazz_name): + var m: Variant = mock(clazz_name, CALL_REAL_FUNC) + assert_that(m)\ + .override_failure_message("The class %s should be mockable" % clazz_name)\ + .is_not_null() + + +func test_mock_by_script_path() -> void: + assert_that(mock(resource_path + "CustomResourceTestClass.gd")).is_not_null() + assert_that(mock(resource_path + "CustomNodeTestClass.gd")).is_not_null() + + +func test_mock_class__overriden_func_get_class() -> void: + assert_that(mock(OverridenGetClassTestClass)).is_not_null() + assert_that(mock(resource_path + "OverridenGetClassTestClass.gd")).is_not_null() + + +func test_mock_fail() -> void: + # not godot class + assert_that(mock("CustomResourceTestClass")).is_null() + # invalid path to script + assert_that(mock("invalid/CustomResourceTestClass.gd")).is_null() + # try to mocking an existing instance is not allowed + assert_that(mock(CustomResourceTestClass.new())).is_null() + + +func test_mock_special_classes() -> void: + var m: JavaClass = mock("JavaClass") + assert_that(m).is_not_null() + + +func test_mock_Node() -> void: + var mocked_node: Variant = mock(Node) + assert_that(mocked_node).is_not_null() + + # test we have initial no interactions checked this mock + verify_no_interactions(mocked_node) + + # verify we have never called 'get_child_count()' + verify(mocked_node, 0).get_child_count() + + # call 'get_child_count()' once + mocked_node.get_child_count() + # verify we have called at once + verify(mocked_node).get_child_count() + + # call function 'get_child_count' a second time + mocked_node.get_child_count() + # verify we have called at twice + verify(mocked_node, 2).get_child_count() + + # test mocked function returns default typed value + assert_that(mocked_node.get_child_count()).is_equal(0) + # now mock return value for function 'foo' to 'overwriten value' + do_return(24).on(mocked_node).get_child_count() + # verify the return value is overwritten + assert_that(mocked_node.get_child_count()).is_equal(24) + + +func test_mock_source_with_class_name_by_resource_path() -> void: + var resource_path_ := 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd' + var m: Variant = mock(resource_path_) + var head :String = m.get_script().source_code.substr(0, 500) + assert_str(head)\ + .contains("class_name DoubledMockClassMunderwoodPathingWorld")\ + .contains("extends '%s'" % resource_path_) + + +func test_mock_source_with_class_name_by_class() -> void: + var resource_path_ := 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd' + var m: Variant = mock(Munderwood_Pathing_World) + var head :String = m.get_script().source_code.substr(0, 500) + assert_str(head)\ + .contains("class_name DoubledMockClassMunderwoodPathingWorld")\ + .contains("extends '%s'" % resource_path_) + + +func test_mock_extends_godot_class() -> void: + var m: Variant = mock(World3D) + var head :String = m.get_script().source_code.substr(0, 500) + assert_str(head)\ + .contains("class_name DoubledMockClassWorld")\ + .contains("extends World3D") + + +var _test_signal_args := Array() +func _emit_ready(...args: Array) -> void: + if args.is_empty(): + return + _test_signal_args = args + + +func test_mock_Node_func_vararg() -> void: + # setup + var mocked_node: Variant = mock(Node) + + # mock return value + do_return(ERR_CANT_CONNECT).on(mocked_node).rpc("test", "arg1", "arg2", "invalid") + do_return(ERR_CANT_OPEN).on(mocked_node).rpc("test", "arg1", "argX", any_string()) + do_return(ERR_CANT_CREATE).on(mocked_node).rpc("test", "arg1", "argX", any_int()) + do_return(OK).on(mocked_node).rpc("test", "arg1", "argX", "arg3") + # verify + assert_that(mocked_node.rpc("test", "arg1", "arg2", "arg3")).is_equal(OK) + assert_that(mocked_node.rpc("test", "arg1", "arg2", "invalid")).is_equal(ERR_CANT_CONNECT) + assert_that(mocked_node.rpc("test", "arg1", "argX", "arg3")).is_equal(OK) + assert_that(mocked_node.rpc("test", "arg1", "argX", "other")).is_equal(ERR_CANT_OPEN) + assert_that(mocked_node.rpc("test", "arg1", "argX", 42)).is_equal(ERR_CANT_CREATE) + + +func test_mock_Node_func_vararg_call_real_func() -> void: + # setup + var mocked_node: Variant = mock(Node, CALL_REAL_FUNC) + assert_that(mocked_node).is_not_null() + assert_that(_test_signal_args).is_empty() + mocked_node.connect("ready", _emit_ready) + + # test emit it + mocked_node.emit_signal("ready", "aa", "bb", "cc") + + # verify is emitted + verify(mocked_node).emit_signal("ready", "aa", "bb", "cc") + await get_tree().process_frame + assert_that(_test_signal_args).is_equal(["aa", "bb", "cc"]) + + # test emit it + mocked_node.emit_signal("ready", "aa", "xxx") + + # verify is emitted + verify(mocked_node).emit_signal("ready", "aa", "xxx") + await get_tree().process_frame + assert_that(_test_signal_args).is_equal(["aa", "xxx"]) + + +class ClassWithSignal: + signal test_signal_a + signal test_signal_b + + func foo(arg :int) -> void: + if arg == 0: + emit_signal(test_signal_a.get_name(), "aa") + else: + emit_signal(test_signal_b.get_name(), "bb", true) + + func bar(arg :int) -> bool: + if arg == 0: + emit_signal(test_signal_a.get_name(), "aa") + else: + emit_signal(test_signal_b.get_name(), "bb", true) + return true + + +func _test_mock_verify_emit_signal() -> void: + var mocked_node: Variant = mock(ClassWithSignal, CALL_REAL_FUNC) + assert_that(mocked_node).is_not_null() + + mocked_node.foo(0) + verify(mocked_node, 1).emit_signal("test_signal_a", "aa") + verify(mocked_node, 0).emit_signal("test_signal_b", "bb", true) + reset(mocked_node) + + mocked_node.foo(1) + verify(mocked_node, 0).emit_signal("test_signal_a", "aa") + verify(mocked_node, 1).emit_signal("test_signal_b", "bb", true) + reset(mocked_node) + + mocked_node.bar(0) + verify(mocked_node, 1).emit_signal("test_signal_a", "aa") + verify(mocked_node, 0).emit_signal("test_signal_b", "bb", true) + reset(mocked_node) + + mocked_node.bar(1) + verify(mocked_node, 0).emit_signal("test_signal_a", "aa") + verify(mocked_node, 1).emit_signal("test_signal_b", "bb", true) + + +func test_mock_custom_class_by_class_name() -> void: + var m: Variant = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + + # test we have initial no interactions checked this mock + verify_no_interactions(m) + # test mocked function returns default typed value + assert_that(m.foo()).is_equal("") + + # now mock return value for function 'foo' to 'overwriten value' + do_return("overriden value").on(m).foo() + # verify the return value is overwritten + assert_that(m.foo()).is_equal("overriden value") + + # now mock return values by custom arguments + do_return("arg_1").on(m).bar(1) + do_return("arg_2").on(m).bar(2) + + assert_that(m.bar(1)).is_equal("arg_1") + assert_that(m.bar(2)).is_equal("arg_2") + + +func test_mock_custom_class_by_resource_path() -> void: + var m: Variant = mock("res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd") + assert_that(m).is_not_null() + + # test we have initial no interactions checked this mock + verify_no_interactions(m) + # test mocked function returns default typed value + assert_that(m.foo()).is_equal("") + + # now mock return value for function 'foo' to 'overwriten value' + do_return("overriden value").on(m).foo() + # verify the return value is overwritten + assert_that(m.foo()).is_equal("overriden value") + + # now mock return values by custom arguments + do_return("arg_1").on(m).bar(1) + do_return("arg_2").on(m).bar(2) + + assert_that(m.bar(1)).is_equal("arg_1") + assert_that(m.bar(2)).is_equal("arg_2") + + +func test_mock_custom_class_func_foo_use_real_func() -> void: + var m: Variant = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # test mocked function returns value from real function + assert_that(m.foo()).is_equal("foo") + # now mock return value for function 'foo' to 'overwriten value' + do_return("overridden value").on(m).foo() + # verify the return value is overwritten + assert_that(m.foo()).is_equal("overridden value") + + +func test_mock_void_func_not_allowed() -> void: + var m: Variant = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + # test mocked void function returns null by default + assert_that(m.foo_void()).is_null() + + # try now mock return value for a void function. results into an error + assert_error(func() -> void: + do_return("overridden value").on(m).foo_void() + ).is_push_error("Mocking functions with return type void is not allowed!") + + +func test_mock_void_call_real_func_not_allowed() -> void: + var m: Variant = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # test mocked void function returns null by default + assert_that(m.foo_void()).is_null() + + # try now mock return value for a void function. results into an error + assert_error(func() -> void: + do_return("overridden value").on(m).foo_void() + ).is_push_error("Mocking functions with return type void is not allowed!") + + +func test_mock_custom_class_func_foo_call_times() -> void: + var m: Variant = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + verify(m, 0).foo() + m.foo() + verify(m, 1).foo() + m.foo() + verify(m, 2).foo() + m.foo() + m.foo() + verify(m, 4).foo() + + +func test_mock_custom_class_func_foo_call_times_real_func() -> void: + var m: Variant = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + verify(m, 0).foo() + m.foo() + verify(m, 1).foo() + m.foo() + verify(m, 2).foo() + m.foo() + m.foo() + verify(m, 4).foo() + + +func test_mock_custom_class_func_foo_full_test() -> void: + var m: Variant = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + verify(m, 0).foo() + assert_that(m.foo()).is_equal("") + verify(m, 1).foo() + do_return("new value").on(m).foo() + verify(m, 1).foo() + assert_that(m.foo()).is_equal("new value") + verify(m, 2).foo() + + +func test_mock_custom_class_func_foo_full_test_real_func() -> void: + var m: Variant = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + verify(m, 0).foo() + assert_that(m.foo()).is_equal("foo") + verify(m, 1).foo() + do_return("new value").on(m).foo() + verify(m, 1).foo() + assert_that(m.foo()).is_equal("new value") + verify(m, 2).foo() + + +func test_mock_custom_class_func_bar() -> void: + var m: Variant = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + assert_that(m.bar(10)).is_equal("") + # verify 'bar' with args [10] is called one time at this point + verify(m, 1).bar(10) + # verify 'bar' with args [10, 20] is never called at this point + verify(m, 0).bar(10, 29) + # verify 'bar' with args [23] is never called at this point + verify(m, 0).bar(23) + + # now mock return value for function 'bar' with args [10] to 'overwriten value' + do_return("overridden value").on(m).bar(10) + # verify the return value is overwritten + assert_that(m.bar(10)).is_equal("overridden value") + # finally verify function call times + verify(m, 2).bar(10) + verify(m, 0).bar(10, 29) + verify(m, 0).bar(23) + + +func test_mock_custom_class_func_bar_real_func() -> void: + var m: Variant = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + assert_that(m.bar(10)).is_equal("test_33") + # verify 'bar' with args [10] is called one time at this point + verify(m, 1).bar(10) + # verify 'bar' with args [10, 20] is never called at this point + verify(m, 0).bar(10, 29) + # verify 'bar' with args [23] is never called at this point + verify(m, 0).bar(23) + + # now mock return value for function 'bar' with args [10] to 'overwriten value' + do_return("overridden value").on(m).bar(10) + # verify the return value is overwritten + assert_that(m.bar(10)).is_equal("overridden value") + # verify the real implementation is used + assert_that(m.bar(10, 29)).is_equal("test_39") + assert_that(m.bar(10, 20, "other")).is_equal("other_30") + # finally verify function call times + verify(m, 2).bar(10) + verify(m, 1).bar(10, 29) + verify(m, 0).bar(10, 20) + verify(m, 1).bar(10, 20, "other") + + +func test_mock_custom_class_func_return_type_enum() -> void: + var m: Variant = mock(ClassWithEnumReturnTypes) + assert_that(m).is_not_null() + verify(m, 0).get_enum() + + # verify enum return default ClassWithEnumReturnTypes.TEST_ENUM.FOO + assert_that(m.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.TEST_ENUM.BAR).on(m).get_enum() + assert_that(m.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.BAR) + verify(m, 2).get_enum() + + # with call real functions + var m2: Variant = mock(ClassWithEnumReturnTypes, CALL_REAL_FUNC) + assert_that(m2).is_not_null() + + # verify enum return type + assert_that(m2.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.TEST_ENUM.BAR).on(m2).get_enum() + assert_that(m2.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.BAR) + + +func test_mock_custom_class_func_return_type_internal_class_enum() -> void: + var m: Variant = mock(ClassWithEnumReturnTypes) + assert_that(m).is_not_null() + verify(m, 0).get_inner_class_enum() + + # verify enum return default + assert_that(m.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR).on(m).get_inner_class_enum() + assert_that(m.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR) + verify(m, 2).get_inner_class_enum() + + # with call real functions + var m2: Variant = mock(ClassWithEnumReturnTypes, CALL_REAL_FUNC) + assert_that(m2).is_not_null() + + # verify enum return type + assert_that(m2.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR).on(m2).get_inner_class_enum() + assert_that(m2.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR) + + +func test_mock_custom_class_func_return_type_external_class_enum() -> void: + var m: Variant = mock(ClassWithEnumReturnTypes) + assert_that(m).is_not_null() + verify(m, 0).get_external_class_enum() + + # verify enum return default + assert_that(m.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.FOO) + do_return(CustomEnums.TEST_ENUM.BAR).on(m).get_external_class_enum() + assert_that(m.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.BAR) + verify(m, 2).get_external_class_enum() + + # with call real functions + var m2: Variant = mock(ClassWithEnumReturnTypes, CALL_REAL_FUNC) + assert_that(m2).is_not_null() + + # verify enum return type + assert_that(m2.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.FOO) + do_return(CustomEnums.TEST_ENUM.BAR).on(m2).get_external_class_enum() + assert_that(m2.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.BAR) + + +func test_mock_custom_class_extends_Node() -> void: + var m: Variant = mock(CustomNodeTestClass) + assert_that(m).is_not_null() + + # test mocked function returns null as default + assert_that(m.get_child_count()).is_equal(0) + assert_that(m.get_children()).contains_exactly([]) + # test seters has no affect + var node :Node = auto_free(Node.new()) + m.add_child(node) + assert_that(m.get_child_count()).is_equal(0) + assert_that(m.get_children()).contains_exactly([]) + verify(m, 1).add_child(node) + verify(m, 2).get_child_count() + verify(m, 2).get_children() + + +func test_mock_custom_class_extends_Node_real_func() -> void: + var m: Variant = mock(CustomNodeTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # test mocked function returns default mock value + assert_that(m.get_child_count()).is_equal(0) + assert_that(m.get_children()).is_equal([]) + # test real seters used + var nodeA :Node = auto_free(Node.new()) + var nodeB :Node = auto_free(Node.new()) + var nodeC :Node = auto_free(Node.new()) + m.add_child(nodeA) + m.add_child(nodeB) + assert_that(m.get_child_count()).is_equal(2) + assert_that(m.get_children()).contains_exactly([nodeA, nodeB]) + verify(m, 1).add_child(nodeA) + verify(m, 1).add_child(nodeB) + verify(m, 0).add_child(nodeC) + verify(m, 2).get_child_count() + verify(m, 2).get_children() + + +func test_mock_custom_class_extends_other_custom_class() -> void: + var m: Variant = mock(CustomClassExtendsCustomClass) + assert_that(mock).is_not_null() + + # foo() form parent class + verify(m, 0).foo() + # foo2() overriden + verify(m, 0).foo2() + # bar2() from class + verify(m, 0).bar2() + + assert_that(m.foo()).is_empty() + assert_that(m.foo2()).is_null() + assert_that(m.bar2()).is_empty() + + verify(m, 1).foo() + verify(m, 1).foo2() + verify(m, 1).bar2() + + # override returns + do_return("abc1").on(m).foo() + do_return("abc2").on(m).foo2() + do_return("abc3").on(m).bar2() + + assert_that(m.foo()).is_equal("abc1") + assert_that(m.foo2()).is_equal("abc2") + assert_that(m.bar2()).is_equal("abc3") + + +func test_mock_custom_class_extends_other_custom_class_call_real_func() -> void: + var m: Variant = mock(CustomClassExtendsCustomClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + + # foo() form parent class + verify(m, 0).foo() + # foo2() overriden + verify(m, 0).foo2() + # bar2() from class + verify(m, 0).bar2() + + assert_that(m.foo()).is_equal("foo") + assert_that(m.foo2()).is_equal("foo2 overriden") + assert_that(m.bar2()).is_equal("test_65") + + verify(m, 1).foo() + verify(m, 1).foo2() + verify(m, 1).bar2() + + # override returns + do_return("abc1").on(m).foo() + do_return("abc2").on(m).foo2() + do_return("abc3").on(m).bar2() + + assert_that(m.foo()).is_equal("abc1") + assert_that(m.foo2()).is_equal("abc2") + assert_that(m.bar2()).is_equal("abc3") + + +func test_mock_static_func() -> void: + var m: Variant = mock(CustomNodeTestClass) + assert_that(m).is_not_null() + # initial not called + verify(m, 0).static_test() + verify(m, 0).static_test_void() + + assert_that(m.static_test()).is_equal("") + assert_that(m.static_test_void()).is_null() + + verify(m, 1).static_test() + verify(m, 1).static_test_void() + m.static_test() + m.static_test_void() + m.static_test_void() + verify(m, 2).static_test() + verify(m, 3).static_test_void() + + +func test_mock_static_func_real_func() -> void: + var m: Variant = mock(CustomNodeTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # initial not called + verify(m, 0).static_test() + verify(m, 0).static_test_void() + + assert_that(m.static_test()).is_equal(CustomNodeTestClass.STATIC_FUNC_RETURN_VALUE) + assert_that(m.static_test_void()).is_null() + + verify(m, 1).static_test() + verify(m, 1).static_test_void() + m.static_test() + m.static_test_void() + m.static_test_void() + verify(m, 2).static_test() + verify(m, 3).static_test_void() + + +func test_mock_custom_class_assert_has_no_side_affect() -> void: + var m: Variant = mock(CustomNodeTestClass) + assert_that(m).is_not_null() + var node := Node.new() + # verify the assertions has no side affect checked mocked object + verify(m, 0).add_child(node) + # expect no change checked childrens + assert_that(m.get_children()).contains_exactly([]) + + m.add_child(node) + # try thre times 'assert_called' to see it has no affect to the mock + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + assert_that(m.get_children()).contains_exactly([]) + # needs to be manually freed + node.free() + + +func test_mock_custom_class_assert_has_no_side_affect_real_func() -> void: + var m: Variant = mock(CustomNodeTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + var node := Node.new() + # verify the assertions has no side affect checked mocked object + verify(m, 0).add_child(node) + # expect no change checked childrens + assert_that(m.get_children()).contains_exactly([]) + + m.add_child(node) + # try thre times 'assert_called' to see it has no affect to the mock + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + assert_that(m.get_children()).contains_exactly([node]) + + +# This test verifies a function is calling other internally functions +# to collect the access times and the override return value is working as expected +func _test_mock_advanced_func_path() -> void: + var m: Variant = mock(AdvancedTestClass, CALL_REAL_FUNC) + # initial nothing is called + verify(m, 0).select(AdvancedTestClass.A) + verify(m, 0).select(AdvancedTestClass.B) + verify(m, 0).select(AdvancedTestClass.C) + verify(m, 0).a() + verify(m, 0).b() + verify(m, 0).c() + + # the function select() swiches based checked input argument to function a(), b() or c() + # call select where called internally func a() and returned "a" + assert_that(m.select(AdvancedTestClass.A)).is_equal("a") + # verify when call select() is also calling original func a() + verify(m, 1).select(AdvancedTestClass.A) + verify(m, 1).a() + + # call select again wiht overriden return value for func a() + do_return("overridden a func").on(m).a() + assert_that(m.select(AdvancedTestClass.A)).is_equal("overridden a func") + + # verify at this time select() and a() is called two times + verify(m, 2).select(AdvancedTestClass.A) + verify(m, 0).select(AdvancedTestClass.B) + verify(m, 0).select(AdvancedTestClass.C) + verify(m, 2).a() + verify(m, 0).b() + verify(m, 0).c() + + # finally use select to switch to internally func c() + assert_that(m.select(AdvancedTestClass.C)).is_equal("c") + verify(m, 2).select(AdvancedTestClass.A) + verify(m, 0).select(AdvancedTestClass.B) + verify(m, 1).select(AdvancedTestClass.C) + verify(m, 2).a() + verify(m, 0).b() + verify(m, 1).c() + + +func _test_mock_godot_class_calls_sub_function() -> void: + var m: Variant = mock(MeshInstance3D, CALL_REAL_FUNC) + verify(m, 0)._mesh_changed() + m.set_mesh(QuadMesh.new()) + verify(m, 1).set_mesh(any_class(Mesh)) + verify(m, 1)._mesh_changed() + + +func test_mock_class_with_inner_class() -> void: + var mock_advanced: Variant = mock(AdvancedTestClass) + assert_that(mock_advanced).is_not_null() + + var mock_a: Variant = mock(AdvancedTestClass.SoundData) + assert_object(mock_a).is_not_null() + + var mock_b: Variant = mock(AdvancedTestClass.AtmosphereData) + assert_object(mock_b).is_not_null() + + var mock_c: Variant = mock(AdvancedTestClass.Area4D) + assert_object(mock_c).is_not_null() + + +func test_mock_class_with_property_getter_and_setter() -> void: + var c :Variant = mock(ClassWithParameterGetterSetter) + + # inital value + assert_int(c._session_count).is_equal(42) + + # overwrite it by 10 + c._session_count = 10 + + # verify the paramater is set to 10 + assert_int(c._session_count).is_equal(10) + # verify the method still returns the default value + assert_int(c.session_count()).is_equal(0) + + # mock the function to return a cutom value + do_return(23).on(c).session_count() + # verify the method now returns the new value + assert_int(c.session_count()).is_equal(23) + + +func test_do_return() -> void: + var mocked_node: Variant = mock(Node) + + # is return 0 by default + mocked_node.get_child_count() + # configure to return 10 when 'get_child_count()' is called + do_return(10).on(mocked_node).get_child_count() + # will now return 10 + assert_int(mocked_node.get_child_count()).is_equal(10) + + # is return 'null' by default + var node: Node = mocked_node.get_child(0) + assert_object(node).is_null() + + # configure to return a mocked 'Camera3D' for child 0 + do_return(mock(Camera3D)).on(mocked_node).get_child(0) + # configure to return a mocked 'Area3D' for child 1 + do_return(mock(Area3D)).on(mocked_node).get_child(1) + + # will now return the Camera3D node + var node0: Node = mocked_node.get_child(0) + assert_object(node0).is_instanceof(Camera3D) + # will now return the Area3D node + var node1: Node = mocked_node.get_child(1) + assert_object(node1).is_instanceof(Area3D) + + +func test_matching_is_sorted() -> void: + var mocked_node: Variant = mock(Node) + do_return(null).on(mocked_node).get_child(any(), false) + do_return(null).on(mocked_node).get_child(1, false) + do_return(null).on(mocked_node).get_child(10, false) + do_return(null).on(mocked_node).get_child(any(), true) + do_return(null).on(mocked_node).get_child(3, true) + + # get the sorted mocked args as array + var mocked_args :Array = mocked_node.__doubler_state().return_values.get("get_child").keys() + assert_array(mocked_args).has_size(5) + + # we expect all argument matchers are sorted to the end + var first_arguments := mocked_args.map(func (v :Array) -> Variant: return v[0]) + assert_int(first_arguments[0]).is_equal(3) + assert_int(first_arguments[1]).is_equal(10) + assert_int(first_arguments[2]).is_equal(1) + assert_object(first_arguments[3]).is_instanceof(GdUnitArgumentMatcher) + assert_object(first_arguments[4]).is_instanceof(GdUnitArgumentMatcher) + + +func test_do_return_with_matchers() -> void: + var mocked_node: Variant = mock(Node) + var childN :Node = auto_free(Node2D.new()) + var child1 :Node = auto_free(Node2D.new()) + var child10 :Node = auto_free(Node2D.new()) + + # for any index return childN by using any() matcher + do_return(childN).on(mocked_node).get_child(any(), false) + # for index 1 and 10 do return 'child1' and 'child10' + do_return(child1).on(mocked_node).get_child(1, false) + do_return(child10).on(mocked_node).get_child(10, false) + # for any index and flag true, we return null by using the 'any_int' matcher + do_return(null).on(mocked_node).get_child(any_int(), true) + + assert_that(mocked_node.get_child(0, true)).is_null() + assert_that(mocked_node.get_child(1, true)).is_null() + assert_that(mocked_node.get_child(2, true)).is_null() + assert_that(mocked_node.get_child(10, true)).is_null() + assert_that(mocked_node.get_child(0)).is_same(childN) + assert_that(mocked_node.get_child(1)).is_same(child1) + assert_that(mocked_node.get_child(2)).is_same(childN) + assert_that(mocked_node.get_child(3)).is_same(childN) + assert_that(mocked_node.get_child(4)).is_same(childN) + assert_that(mocked_node.get_child(5)).is_same(childN) + assert_that(mocked_node.get_child(6)).is_same(childN) + assert_that(mocked_node.get_child(7)).is_same(childN) + assert_that(mocked_node.get_child(8)).is_same(childN) + assert_that(mocked_node.get_child(9)).is_same(childN) + assert_that(mocked_node.get_child(10)).is_same(child10) + + +func test_example_verify() -> void: + var mocked_node: Variant = mock(Node) + + # verify we have no interactions currently checked this instance + verify_no_interactions(mocked_node) + + # call with different arguments + mocked_node.set_process(false) # 1 times + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + # verify how often we called the function with different argument + verify(mocked_node, 2).set_process(true) # in sum two times with true + verify(mocked_node, 1).set_process(false)# in sum one time with false + + # verify total sum by using an argument matcher + verify(mocked_node, 3).set_process(any_bool()) + + +func test_verify_fail() -> void: + var mocked_node: Variant = mock(Node) + + # interact two time + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + # verify we interacts two times + verify(mocked_node, 2).set_process(true) + + # verify should fail because we interacts two times and not one + var expected_error := """ + Expecting interaction on: + 'set_process(true :bool)' 1 time's + But found interactions on: + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n").replace("\r", "") + assert_failure(func() -> void: verify(mocked_node, 1).set_process(true)) \ + .is_failed() \ + .has_message(expected_error) + + +func test_verify_func_interaction_wiht_PoolStringArray() -> void: + var mocked: Variant = mock(ClassWithPoolStringArrayFunc) + + mocked.set_values(PackedStringArray()) + + verify(mocked).set_values(PackedStringArray()) + verify_no_more_interactions(mocked) + + +func test_verify_func_interaction_wiht_PoolStringArray_fail() -> void: + var mocked: Variant = mock(ClassWithPoolStringArrayFunc) + + mocked.set_values(PackedStringArray()) + + # try to verify with default array type instead of PackedStringArray type + var expected_error := """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's""" \ + .dedent().trim_prefix("\n").replace("\r", "") + assert_failure(func() -> void: verify(mocked, 1).set_values([])) \ + .is_failed() \ + .has_message(expected_error) + + reset(mocked) + # try again with called two times and different args + mocked.set_values(PackedStringArray()) + mocked.set_values(PackedStringArray(["a", "b"])) + mocked.set_values([1, 2]) + expected_error = """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's + 'set_values(["a", "b"] :PackedStringArray)' 1 time's + 'set_values([1, 2] :Array)' 1 time's""" \ + .dedent().trim_prefix("\n").replace("\r", "") + assert_failure(func() -> void: verify(mocked, 1).set_values([])) \ + .is_failed() \ + .has_message(expected_error) + + +func test_reset() -> void: + var mocked_node: Variant = mock(Node) + + # call with different arguments + mocked_node.set_process(false) # 1 times + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + verify(mocked_node, 2).set_process(true) + verify(mocked_node, 1).set_process(false) + + # now reset the mock + reset(mocked_node) + # verify all counters have been reset + verify_no_interactions(mocked_node) + + +func test_verify_no_interactions() -> void: + var mocked_node: Variant = mock(Node) + + # verify we have no interactions checked this mock + verify_no_interactions(mocked_node) + + +func test_verify_no_interactions_fails() -> void: + var mocked_node: Variant = mock(Node) + + # interact + mocked_node.set_process(false) # 1 times + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + var expected_error :=""" + Expecting no more interactions! + But found interactions on: + 'set_process(false :bool)' 1 time's + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n") + # it should fail because we have interactions + assert_failure(func() -> void: verify_no_interactions(mocked_node)) \ + .is_failed() \ + .has_message(expected_error) + + +func test_verify_no_more_interactions() -> void: + var mocked_node: Variant = mock(Node) + + mocked_node.is_ancestor_of(null) + mocked_node.set_process(false) + mocked_node.set_process(true) + mocked_node.set_process(true) + + # verify for called functions + verify(mocked_node, 1).is_ancestor_of(null) + verify(mocked_node, 2).set_process(true) + verify(mocked_node, 1).set_process(false) + + # There should be no more interactions checked this mock + verify_no_more_interactions(mocked_node) + + +func test_verify_no_more_interactions_but_has() -> void: + var mocked_node: Variant = mock(Node) + + mocked_node.is_ancestor_of(null) + mocked_node.set_process(false) + mocked_node.set_process(true) + mocked_node.set_process(true) + + # now we simulate extra calls that we are not explicit verify + mocked_node.is_inside_tree() + mocked_node.is_inside_tree() + # a function with default agrs + mocked_node.find_child("mask") + # same function again with custom agrs + mocked_node.find_child("mask", false, false) + + # verify 'all' exclusive the 'extra calls' functions + verify(mocked_node, 1).is_ancestor_of(null) + verify(mocked_node, 2).set_process(true) + verify(mocked_node, 1).set_process(false) + + # now use 'verify_no_more_interactions' to check we have no more interactions checked this mock + # but should fail with a collecion of all not validated interactions + var expected_error :=""" + Expecting no more interactions! + But found interactions on: + 'is_inside_tree()' 2 time's + 'find_child(mask :String, true :bool, true :bool)' 1 time's + 'find_child(mask :String, false :bool, false :bool)' 1 time's""" \ + .dedent().trim_prefix("\n") + assert_failure(func() -> void: verify_no_more_interactions(mocked_node)) \ + .is_failed() \ + .has_message(expected_error) + + +func test_mock_snake_case_named_class_by_resource_path() -> void: + var mock_a: Variant = mock("res://addons/gdUnit4/test/mocker/resources/snake_case.gd") + assert_object(mock_a).is_not_null() + + mock_a.custom_func() + verify(mock_a).custom_func() + verify_no_more_interactions(mock_a) + + var mock_b: Variant = mock("res://addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd") + assert_object(mock_b).is_not_null() + + mock_b.custom_func() + verify(mock_b).custom_func() + verify_no_more_interactions(mock_b) + + +func test_mock_snake_case_named_godot_class_by_name() -> void: + # try checked Godot class + var mocked_tcp_server: Variant = mock("TCPServer") + assert_object(mocked_tcp_server).is_not_null() + + mocked_tcp_server.is_listening() + mocked_tcp_server.is_connection_available() + verify(mocked_tcp_server).is_listening() + verify(mocked_tcp_server).is_connection_available() + verify_no_more_interactions(mocked_tcp_server) + + +func test_mock_snake_case_named_class_by_class() -> void: + var m: Variant = mock(snake_case_class_name) + assert_object(m).is_not_null() + + m.custom_func() + verify(m).custom_func() + verify_no_more_interactions(m) + + # try checked Godot class + var mocked_tcp_server: Variant = mock(TCPServer) + assert_object(mocked_tcp_server).is_not_null() + + mocked_tcp_server.is_listening() + mocked_tcp_server.is_connection_available() + verify(mocked_tcp_server).is_listening() + verify(mocked_tcp_server).is_connection_available() + verify_no_more_interactions(mocked_tcp_server) + + +func test_mock_func_with_default_build_in_type() -> void: + var m: Variant = mock(ClassWithDefaultBuildIntTypes) + assert_object(m).is_not_null() + # call with default arg + m.foo("abc") + m.bar("def") + verify(m).foo("abc", Color.RED) + verify(m).bar("def", Vector3.FORWARD, AABB()) + verify_no_more_interactions(m) + + # call with custom color arg + m.foo("abc", Color.BLUE) + m.bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + verify(m).foo("abc", Color.BLUE) + verify(m).bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + verify_no_more_interactions(m) + + +func test_mock_virtual_function_is_not_called_twice() -> void: + # this test verifies the special handling of virtual functions by Godot + # virtual functions are handeld in a special way + # node.cpp + # case NOTIFICATION_READY: { + # + # if (get_script_instance()) { + # + # Variant::CallError err; + # get_script_instance()->call_multilevel_reversed(SceneStringNames::get_singleton()->_ready,NULL,0); + # } + + var m: Variant = mock(ClassWithOverridenVirtuals, CALL_REAL_FUNC) + assert_object(m).is_not_null() + + # inital constructor + assert_that(m._x).is_equal("_init") + + # add_child calls internally by "default" _ready() where is a virtual function + @warning_ignore("unsafe_cast") + add_child(m as Node) + + # verify _ready func is only once called + assert_that(m._x).is_equal("_ready") + + # now simulate an input event calls '_input' + var action := InputEventKey.new() + action.pressed = false + action.keycode = KEY_ENTER + get_tree().root.push_input(action) + assert_that(m._x).is_equal("ui_accept") + + +func test_mock_scene_by_path() -> void: + var mocked_scene: Variant = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(mocked_scene).is_not_null() + assert_object(mocked_scene.get_script()).is_not_null() + assert_str(mocked_scene.get_script().resource_name).starts_with("MockTestScene") + # check is mocked scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(mocked_scene)).is_true() + + +func test_mock_scene_by_resource() -> void: + var resource: Object = load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var mocked_scene: Variant = mock(resource) + assert_object(mocked_scene).is_not_null() + assert_object(mocked_scene.get_script()).is_not_null() + assert_str(mocked_scene.get_script().resource_name).starts_with("MockTestScene") + # check is mocked scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(mocked_scene)).is_true() + + +func test_mock_scene_by_instance() -> void: + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var instance :Control = auto_free(resource.instantiate()) + var mocked_scene: Variant = mock(instance) + # must fail mock an instance is not allowed + assert_object(mocked_scene).is_null() + + +func test_mock_scene_by_path_fail_has_no_script_attached() -> void: + var mocked_scene: Variant = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn") + assert_object(mocked_scene).is_null() + + +func test_mock_scene_variables_is_set() -> void: + var mocked_scene: Variant = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(mocked_scene).is_not_null() + + # Add as child to a node to trigger _ready to initalize all variables + @warning_ignore("unsafe_cast") + add_child(mocked_scene as Node) + assert_object(mocked_scene._box1).is_not_null() + assert_object(mocked_scene._box2).is_not_null() + assert_object(mocked_scene._box3).is_not_null() + + # check signals are connected + @warning_ignore("unsafe_cast") + assert_bool(mocked_scene.is_connected("panel_color_change", Callable(mocked_scene as Object, "_on_panel_color_changed"))) + + # check exports + assert_str(mocked_scene._initial_color.to_html()).is_equal(Color.RED.to_html()) + + +func test_mock_scene_execute_func_yielded() -> void: + var mocked_scene: Variant = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(mocked_scene).is_not_null() + @warning_ignore("unsafe_cast") + add_child(mocked_scene as Node) + # execute the 'color_cycle' func where emits three signals + # using yield to wait for function is completed + var result :String = await mocked_scene.color_cycle() + # verify the return value of 'color_cycle' + assert_str(result).is_equal("black") + + verify(mocked_scene)._on_panel_color_changed(mocked_scene._box1, Color.RED) + verify(mocked_scene)._on_panel_color_changed(mocked_scene._box1, Color.BLUE) + verify(mocked_scene)._on_panel_color_changed(mocked_scene._box1, Color.GREEN) + + +class Base: + func _init(_value :String) -> void: + pass + + +class Foo extends Base: + func _init() -> void: + super("test") + + +func test_mock_with_inheritance_method() -> void: + var foo: Variant = mock(Foo) + assert_object(foo).is_not_null() + + +func test_mock_func_default_arg_dict() -> void: + var mock_obj :ClassWithDictionaryDefaultArguments = mock(ClassWithDictionaryDefaultArguments) + + do_return(["a", "b"]).on(mock_obj).on_dictionary_case1(any_dictionary()) + verify_no_interactions(mock_obj) + + assert_array(mock_obj.on_dictionary_case1({})).contains_exactly(["a", "b"]) + verify(mock_obj).on_dictionary_case1({}) + + +func test_mock_with_variant_as_defaults() -> void: + var mock_obj: Variant = mock(ClassWithVariantDefaultArguments) + assert_object(mock_obj).is_not_null() + + +# https://github.com/MikeSchulze/gdUnit4/issues/742 +func test_mock_with_await_function() -> void: + @warning_ignore("unsafe_cast") + var mocked_instance := mock(ClassWithAwaitFunc, CALL_REAL_FUNC) as ClassWithAwaitFunc + add_child(mocked_instance) + + await mocked_instance.await_function() + + verify(mocked_instance, 1).await_function() diff --git a/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd.uid b/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd.uid new file mode 100644 index 0000000..49ff933 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd.uid @@ -0,0 +1 @@ +uid://71s3yyhe3yia diff --git a/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd b/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd new file mode 100644 index 0000000..96d13e7 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd @@ -0,0 +1,53 @@ +# fuzzer to get available godot class names +class_name GodotClassNameFuzzer +extends Fuzzer + +var class_names :Array[String] = [] + +const EXCLUDED_CLASSES = [ + "JavaClass", + "GDScript", + "_ClassDB", + "MainLoop", + "JNISingleton", + "SceneTree", + "WebRTC", + "WebRTCPeerConnection", + "Tween", + "TextServerAdvanced", + "InputEventShortcut", + "FramebufferCacheRD", + "UniformSetCacheRD", + # GD-110 - missing enum `Vector3.Axis` + "Sprite3D", "AnimatedSprite3D", "LookAtModifier3D", + # Godot-4-4_dev5 unknown classes + "AnimationNodeStartState", + "AnimationNodeEndState", + # Godot-4-4_dev7 get_class issues + "UPNPDevice", + "UPNP" +] + + +func _init(no_singleton :bool = false, only_instancialbe :bool = false) -> void: + #class_names = ClassDB.get_class_list() + for clazz_name in ClassDB.get_class_list(): + #https://github.com/godotengine/godot/issues/67643 + if clazz_name.contains("Extension"): + continue + if no_singleton and Engine.has_singleton(clazz_name): + continue + if only_instancialbe and not ClassDB.can_instantiate(clazz_name): + continue + # exclude special classes + if EXCLUDED_CLASSES.has(clazz_name): + continue + # exlude Godot 3.5 *Tweener classes where produces and error + # `ERROR: Can't create empty IntervalTweener. Use get_tree().tween_property() or tween_property() instead.` + if clazz_name.find("Tweener") != -1: + continue + class_names.push_back(clazz_name) + + +func next_value() -> String: + return class_names[randi() % class_names.size()] diff --git a/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd.uid b/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd.uid new file mode 100644 index 0000000..a350213 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd.uid @@ -0,0 +1 @@ +uid://1utdgk1j2yhk diff --git a/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd b/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd new file mode 100644 index 0000000..04ddfbc --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd @@ -0,0 +1,86 @@ +# this class used to mock testing with inner classes and default arguments in functions +class_name AdvancedTestClass +extends Resource + +class SoundData: + @warning_ignore("unused_private_class_variable") + var _sample :String + @warning_ignore("unused_private_class_variable") + var _randomnes :float + +class AtmosphereData: + enum { + WATER, + AIR, + SMOKY, + } + var _toxigen :float + var _type :int + + func _init(type := AIR, toxigen := 0.0) -> void: + _type = type + _toxigen = toxigen +# some comment, and an row staring with an space to simmulate invalid formatting + + + # seter func with default values + func set_data(type := AIR, toxigen := 0.0) -> void: + _type = type + _toxigen = toxigen + + static func to_atmosphere(_value :Dictionary) -> AtmosphereData: + return null + +class Area4D extends Resource: + + const SOUND := 1 + const ATMOSPHERE := 2 + var _meta := Dictionary() + + func _init(_x :int, atmospere :AtmosphereData = null) -> void: + _meta[ATMOSPHERE] = atmospere + + func get_sound() -> SoundData: + # sounds are optional + if _meta.has(SOUND): + @warning_ignore("unsafe_cast") + return _meta[SOUND] as SoundData + return null + + func get_atmoshere() -> AtmosphereData: + @warning_ignore("unsafe_cast") + return _meta[ATMOSPHERE] as AtmosphereData + +var _areas : = {} + +func _init() -> void: + # add default atmoshere + _areas["default"] = Area4D.new(1, AtmosphereData.new()) + +func get_area(name :String, default :Area4D = null) -> Area4D: + return _areas.get(name, default) + + +# test spy is called sub functions select() -> a(), b(), c() +enum { + A, B, C +} + +func a() -> String: + return "a" + +func b() -> String: + return "b" + +func c() -> String: + return "c" + +func select( type :int) -> String: + match type: + A: return a() + B: return b() + C: return c() + _: return "" + +static func to_foo() -> String: + return "foo" diff --git a/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd.uid b/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd.uid new file mode 100644 index 0000000..3ccb19d --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd.uid @@ -0,0 +1 @@ +uid://yp54vtf06gs diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithAwaitFunc.gd b/addons/gdUnit4/test/mocker/resources/ClassWithAwaitFunc.gd new file mode 100644 index 0000000..169cd5b --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithAwaitFunc.gd @@ -0,0 +1,15 @@ +class_name ClassWithAwaitFunc +extends Node + + +func normal_function() -> void: + print("normal") + + +func await_function() -> void: + print(await _await_function()) + + +func _await_function() -> String: + await get_tree().process_frame + return "test" diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithAwaitFunc.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithAwaitFunc.gd.uid new file mode 100644 index 0000000..78b49be --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithAwaitFunc.gd.uid @@ -0,0 +1 @@ +uid://5511koi218nq diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd b/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd new file mode 100644 index 0000000..a51a32c --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd @@ -0,0 +1,3 @@ +class_name GdUnit_Test_CustomClassName +extends Resource + diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd.uid new file mode 100644 index 0000000..b943305 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd.uid @@ -0,0 +1 @@ +uid://1wbgrdnbtcfy diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd b/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd new file mode 100644 index 0000000..7e925d7 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd @@ -0,0 +1,52 @@ +extends Object + +var _message :String + +@warning_ignore("unused_parameter") +func _init(message:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) -> void: + _message = message + + +@warning_ignore("unused_parameter") +func a1(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) -> void: + pass + + +@warning_ignore("unused_parameter") +func a2(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) -> void: + pass + + +@warning_ignore("unused_parameter") +func a3(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) -> void: + pass + + +@warning_ignore("unused_parameter") +func a4(set_name:String, + path:String="", + load_on_init:bool=false, + set_auto_save:bool=false, + set_network_sync:bool=false +) -> void: + pass + + +@warning_ignore("unused_parameter") +func a5( + value : Array, + expected : String, + test_parameters : Array = [ + [ ["a"], "a" ], + [ ["a", "very", "long", "argument"], "a very long argument" ], + ] +) -> void: + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd.uid new file mode 100644 index 0000000..cfd2b1b --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd.uid @@ -0,0 +1 @@ +uid://mi4l6d6geejo diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd b/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd new file mode 100644 index 0000000..a4cfb07 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd @@ -0,0 +1,8 @@ +class_name ClassWithDefaultBuildIntTypes +extends RefCounted + +func foo(_value :String, _color := Color.RED) -> void: + pass + +func bar(_value :String, _direction := Vector3.FORWARD, _aabb := AABB()) -> void: + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd.uid new file mode 100644 index 0000000..c06cb31 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd.uid @@ -0,0 +1 @@ +uid://dgnmk2b7kt5v4 diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd b/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd new file mode 100644 index 0000000..3d1cc73 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd @@ -0,0 +1,26 @@ +class_name ClassWithEnumReturnTypes +extends Resource + + +class InnerClass: + enum TEST_ENUM { FOO = 111, BAR = 222 } + +enum TEST_ENUM { FOO = 1, BAR = 2 } + +const NOT_AN_ENUM := 1 + +const X := { FOO=1, BAR=2 } + + +func get_enum() -> TEST_ENUM: + return TEST_ENUM.FOO + + +# function signature with an external enum reference +func get_external_class_enum() -> CustomEnums.TEST_ENUM: + return CustomEnums.TEST_ENUM.FOO + + +# function signature with an inner class enum reference +func get_inner_class_enum() -> InnerClass.TEST_ENUM: + return InnerClass.TEST_ENUM.FOO diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd.uid new file mode 100644 index 0000000..932d924 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd.uid @@ -0,0 +1 @@ +uid://kayp1vv6m1of diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithMultilineBlocks.gd b/addons/gdUnit4/test/mocker/resources/ClassWithMultilineBlocks.gd new file mode 100644 index 0000000..a8ca91c --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithMultilineBlocks.gd @@ -0,0 +1,29 @@ +extends RefCounted + + +func no_arg_multiline( +) -> void: + pass + + +func single_arg_multiline( + _arg1: int +) -> void: + pass + + +func multi_arg_multiline( + _arg1: int, + _arg2: String, + _arg3: String +) -> void: + pass + + +## See https://github.com/godot-gdunit-labs/gdUnit4/issues/1096 +func multi_arg_with_backslashes_multiline(\ + _arg1: int,\ + _arg2: String,\ + _arg3: String\ +) -> void: + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithMultilineBlocks.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithMultilineBlocks.gd.uid new file mode 100644 index 0000000..1bbf105 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithMultilineBlocks.gd.uid @@ -0,0 +1 @@ +uid://qriwia6poaag diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd b/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd new file mode 100644 index 0000000..dbdde7b --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd @@ -0,0 +1,5 @@ +class_name ClassWithNameA +extends Resource + +class InnerClass extends Resource: + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd.uid new file mode 100644 index 0000000..af9c10a --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd.uid @@ -0,0 +1 @@ +uid://cg18o6kw2t0xk diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd b/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd new file mode 100644 index 0000000..fb50778 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd @@ -0,0 +1,8 @@ +# some comments and empty lines + +# similate bad formated class +# + +class_name ClassWithNameB +extends Resource + diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd.uid new file mode 100644 index 0000000..377ff53 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd.uid @@ -0,0 +1 @@ +uid://vpe5b8dpfcyy diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd b/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd new file mode 100644 index 0000000..1901de9 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd @@ -0,0 +1,21 @@ +class_name ClassWithOverridenVirtuals +extends Node + +var _x := "default" + + +func _init() -> void: + _x = "_init" + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + if _x == "_init": + _x = "" + _x += "_ready" + + +func _input(event :InputEvent) -> void: + _x = "_input" + if event.is_action_released("ui_accept"): + _x = "ui_accept" diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd.uid new file mode 100644 index 0000000..2e906e5 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd.uid @@ -0,0 +1 @@ +uid://db3hhtafjp70f diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithParameterGetterSetter.gd b/addons/gdUnit4/test/mocker/resources/ClassWithParameterGetterSetter.gd new file mode 100644 index 0000000..078fc68 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithParameterGetterSetter.gd @@ -0,0 +1,13 @@ +class_name ClassWithParameterGetterSetter +extends RefCounted + + +var _session_count: int = 42: + get: + return _session_count + set(value): + _session_count = value + + +func session_count() -> int: + return _session_count diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithParameterGetterSetter.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithParameterGetterSetter.gd.uid new file mode 100644 index 0000000..6d5832e --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithParameterGetterSetter.gd.uid @@ -0,0 +1 @@ +uid://cwv4tnwqivxes diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd b/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd new file mode 100644 index 0000000..5a53eb8 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd @@ -0,0 +1,7 @@ +class_name ClassWithPoolStringArrayFunc +extends RefCounted + +var _values :PackedStringArray + +func set_values(values :PackedStringArray) -> void: + _values = values diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd.uid new file mode 100644 index 0000000..f68fa23 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd.uid @@ -0,0 +1 @@ +uid://cj7i25euifqho diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd b/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd new file mode 100644 index 0000000..64acb33 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd @@ -0,0 +1,40 @@ +@tool +class_name ClassWithVariables +extends Node + +enum{ + A, + B, + C +} + +enum ETYPE { + EA, + EB + } + +# Declare member variables here. Examples: +var a := 2 +var b := "text" + +# Declare some const variables +const T1 = 1 + +const T2 = 2 + +signal source_changed( text:String ) + +@onready var name_label := load("res://addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd") + +@export var path: NodePath = ".." + +class ClassA: + var x := 1 + # some comment + func foo()->String: + return "" + + +func foo(_value :int = T1) -> void: + var _c := str(a) + b + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd.uid new file mode 100644 index 0000000..53c029b --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd.uid @@ -0,0 +1 @@ +uid://b8kbuu13plpfr diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithVariantDefaultArguments.gd b/addons/gdUnit4/test/mocker/resources/ClassWithVariantDefaultArguments.gd new file mode 100644 index 0000000..d399549 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithVariantDefaultArguments.gd @@ -0,0 +1,9 @@ +class_name ClassWithVariantDefaultArguments +extends Resource + +@warning_ignore("unused_parameter") +func add_button( + ucids: Array[int], position: Vector2i, size: Vector2i, style: int, text: Variant = "", + button_name := "", type_in := 0, caption := "", show_everywhere := false +) -> void: + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithVariantDefaultArguments.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithVariantDefaultArguments.gd.uid new file mode 100644 index 0000000..045af89 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithVariantDefaultArguments.gd.uid @@ -0,0 +1 @@ +uid://d2yax61mqn6ln diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd new file mode 100644 index 0000000..2156f64 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd @@ -0,0 +1,4 @@ +# some comment + +extends Resource + diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd.uid new file mode 100644 index 0000000..fb1dcab --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd.uid @@ -0,0 +1 @@ +uid://bujc8iga81mcm diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd new file mode 100644 index 0000000..3cc60d1 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd @@ -0,0 +1,5 @@ +# some comment + +func foo() -> void: + pass + diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd.uid b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd.uid new file mode 100644 index 0000000..89e895e --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd.uid @@ -0,0 +1 @@ +uid://uide58aijfba diff --git a/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd b/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd new file mode 100644 index 0000000..90e030f --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd @@ -0,0 +1,10 @@ +class_name CustomClassExtendsCustomClass +extends CustomResourceTestClass + +# override +@warning_ignore("untyped_declaration") +func foo2(): + return "foo2 overriden" + +func bar2() -> String: + return bar(23, 42) diff --git a/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd.uid b/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd.uid new file mode 100644 index 0000000..0db1aac --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd.uid @@ -0,0 +1 @@ +uid://bg6x4burtbat3 diff --git a/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd b/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd new file mode 100644 index 0000000..4aec1e5 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd @@ -0,0 +1,30 @@ +class_name CustomNodeTestClass +extends Node + +const STATIC_FUNC_RETURN_VALUE = "i'm a static function" + +enum { + ENUM_A, + ENUM_B +} + +#func get_path() -> NodePath: +# return NodePath() + +#func duplicate(flags_=15) -> Node: +# return self + +# added a custom static func for mock testing +static func static_test() -> String: + return STATIC_FUNC_RETURN_VALUE + +static func static_test_void() -> void: + pass + +func get_value( type := ENUM_A) -> int: + match type: + ENUM_A: + return 0 + ENUM_B: + return 1 + return -1 diff --git a/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd.uid b/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd.uid new file mode 100644 index 0000000..8414afa --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd.uid @@ -0,0 +1 @@ +uid://dkxe03fl6wys8 diff --git a/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd b/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd new file mode 100644 index 0000000..390fcca --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd @@ -0,0 +1,18 @@ +class_name CustomResourceTestClass +extends Resource + +func foo() -> String: + return "foo" + +func foo2() -> Variant: + return "foo2" + +func foo_void() -> void: + pass + +func bar(arg1 :int, arg2 :int = 23, name :String = "test") -> String: + return "%s_%d" % [name, arg1+arg2] + +@warning_ignore("untyped_declaration") +func foo5(): + pass diff --git a/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd.uid b/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd.uid new file mode 100644 index 0000000..d5b1e51 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd.uid @@ -0,0 +1 @@ +uid://bgwgm5gfkwqlj diff --git a/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd b/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd new file mode 100644 index 0000000..9815eab --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd @@ -0,0 +1,16 @@ +class_name DeepStubTestClass + +class XShape: + var _shape : Shape3D = BoxShape3D.new() + + func get_shape() -> Shape3D: + return _shape + + +var _shape :XShape + +func add(shape :XShape) -> void: + _shape = shape + +func validate() -> bool: + return _shape.get_shape().get_margin() == 0.0 diff --git a/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd.uid b/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd.uid new file mode 100644 index 0000000..48e6a21 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd.uid @@ -0,0 +1 @@ +uid://c2ucwm1h74ou3 diff --git a/addons/gdUnit4/test/mocker/resources/GD-256/world.gd b/addons/gdUnit4/test/mocker/resources/GD-256/world.gd new file mode 100644 index 0000000..e1a1fd8 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/GD-256/world.gd @@ -0,0 +1,5 @@ +class_name Munderwood_Pathing_World +extends Node + +func foo() -> String: + return "test" diff --git a/addons/gdUnit4/test/mocker/resources/GD-256/world.gd.uid b/addons/gdUnit4/test/mocker/resources/GD-256/world.gd.uid new file mode 100644 index 0000000..9aff34d --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/GD-256/world.gd.uid @@ -0,0 +1 @@ +uid://cd6b3ui55go13 diff --git a/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd b/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd new file mode 100644 index 0000000..5ae8a1a --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd @@ -0,0 +1,11 @@ +class_name OverridenGetClassTestClass +extends Resource + + +@warning_ignore("native_method_override") +func get_class() -> String: + return "OverridenGetClassTestClass" + +func foo() -> String: + prints("foo") + return "foo" diff --git a/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd.uid b/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd.uid new file mode 100644 index 0000000..bd9c2ca --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd.uid @@ -0,0 +1 @@ +uid://bxjgu7j06x2pa diff --git a/addons/gdUnit4/test/mocker/resources/TestPerson.gd b/addons/gdUnit4/test/mocker/resources/TestPerson.gd new file mode 100644 index 0000000..933ecaa --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/TestPerson.gd @@ -0,0 +1,23 @@ +class_name TestPerson +extends Object + +var _name :String +var _value :int +var _address :Address + +class Address: + var _street :String + var _code :int + + func _init(street :String, code :int) -> void: + _street = street + _code = code + + +func _init(name_ :String, street :String, code :int) -> void: + _name = name_ + _value = 1024 + _address = Address.new(street, code) + +func name() -> String: + return _name diff --git a/addons/gdUnit4/test/mocker/resources/TestPerson.gd.uid b/addons/gdUnit4/test/mocker/resources/TestPerson.gd.uid new file mode 100644 index 0000000..63851a8 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/TestPerson.gd.uid @@ -0,0 +1 @@ +uid://dh0g1jplv10cd diff --git a/addons/gdUnit4/test/mocker/resources/capsuleshape2d.tres b/addons/gdUnit4/test/mocker/resources/capsuleshape2d.tres new file mode 100644 index 0000000..de1187d --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/capsuleshape2d.tres @@ -0,0 +1,3 @@ +[gd_resource type="CapsuleShape2D" format=2] + +[resource] diff --git a/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd b/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd new file mode 100644 index 0000000..16cd2d4 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd @@ -0,0 +1,39 @@ +class_name Spell +extends Node + +signal spell_explode(spell: Spell) + +const SPELL_LIVE_TIME = 1000 + +@warning_ignore("unused_private_class_variable") +var _spell_fired :bool = false +var _spell_live_time :float = 0 +var _spell_pos :Vector3 = Vector3.ZERO + +# helper counter for testing simulate_frames +@warning_ignore("unused_private_class_variable") +var _debug_process_counted := 0 + +func _ready() -> void: + set_name("Spell") + +# only comment in for debugging reasons +#func _notification(what): +# prints("Spell", GdObjects.notification_as_string(what)) + +func _process(delta :float) -> void: + # added pseudo yield to check `simulate_frames` works wih custom yielding + await get_tree().process_frame + _spell_live_time += delta * 1000 + if _spell_live_time < SPELL_LIVE_TIME: + move(delta) + else: + explode() + +func move(delta :float) -> void: + #await get_tree().create_timer(0.1).timeout + _spell_pos.x += delta + +func explode() -> void: + spell_explode.emit(self) + diff --git a/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd.uid b/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd.uid new file mode 100644 index 0000000..da9422d --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd.uid @@ -0,0 +1 @@ +uid://bokrljn0afija diff --git a/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd new file mode 100644 index 0000000..025c7f7 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd @@ -0,0 +1,124 @@ +extends Control + +signal panel_color_change(box :ColorRect, color :Color) + +const COLOR_CYCLE := [Color.ROYAL_BLUE, Color.CHARTREUSE, Color.YELLOW_GREEN] + +@onready var _box1 := $VBoxContainer/PanelContainer/HBoxContainer/Panel1 +@onready var _box2 := $VBoxContainer/PanelContainer/HBoxContainer/Panel2 +@onready var _box3 := $VBoxContainer/PanelContainer/HBoxContainer/Panel3 + +@warning_ignore("unused_private_class_variable") +@export var _initial_color := Color.RED + +@warning_ignore("unused_private_class_variable") +var _nullable :Object + +var _last_pressed_strength: float + + +func _ready() -> void: + panel_color_change.connect(_on_panel_color_changed) + # we call this function to verify the _ready is only once called + # this is need to verify `add_child` is calling the original implementation only once + only_one_time_call() + + +func only_one_time_call() -> void: + pass + + +#func _notification(what): +# prints("TestScene", GdObjects.notification_as_string(what)) + + +func _on_test_pressed(button_id :int) -> void: + var box :ColorRect + match button_id: + 1: box = _box1 + 2: box = _box2 + 3: box = _box3 + panel_color_change.emit(box, Color.RED) + # special case for button 3 we wait 1s to change to gray + if button_id == 3: + await get_tree().create_timer(1).timeout + panel_color_change.emit(box, Color.GRAY) + + +func _on_panel_color_changed(box :ColorRect, color :Color) -> void: + box.color = color + + +func create_timer(timeout :float) -> Timer: + var timer :Timer = Timer.new() + add_child(timer) + timer.timeout.connect(_on_timeout.bind(timer)) + timer.set_one_shot(true) + timer.start(timeout) + return timer + + +func _on_timeout(timer :Timer) -> void: + remove_child(timer) + timer.queue_free() + + +func color_cycle() -> String: + prints("color_cycle") + await create_timer(0.500).timeout + emit_signal("panel_color_change", _box1, Color.RED) + prints("timer1") + await create_timer(0.500).timeout + emit_signal("panel_color_change", _box1, Color.BLUE) + prints("timer2") + await create_timer(0.500).timeout + emit_signal("panel_color_change", _box1, Color.GREEN) + prints("cycle end") + return "black" + + +func start_color_cycle() -> void: + color_cycle() + + +# used for manuall spy checked created spy +func _create_spell() -> Spell: + return Spell.new() + + +func create_spell() -> Spell: + var spell := _create_spell() + spell.connect("spell_explode", Callable(self, "_destroy_spell")) + return spell + + +func _destroy_spell(spell :Spell) -> void: + #prints("_destroy_spell", spell) + remove_child(spell) + spell.queue_free() + + +func _input(event :InputEvent) -> void: + if event.is_action_released("ui_accept"): + add_child(create_spell()) + + if event is InputEventAction: + _last_pressed_strength = (event as InputEventAction).strength + + #prints(event.as_text()) + + +func add(a: int, b :int) -> int: + return a + b + + +func _on_touch_1_pressed() -> void: + pass + + +func _on_touch_1_released() -> void: + pass + + +func _on_exit_pressed() -> void: + get_tree().quit() diff --git a/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd.uid b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd.uid new file mode 100644 index 0000000..965d876 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd.uid @@ -0,0 +1 @@ +uid://b1vr3aidvpxrm diff --git a/addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn new file mode 100644 index 0000000..31e1c7b --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn @@ -0,0 +1,134 @@ +[gd_scene load_steps=3 format=3 uid="uid://bf24pr1xj60o6"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd" id="1"] +[ext_resource type="Texture2D" path="res://addons/gdUnit4/src/core/assets/touch-button.png" id="2_xgglm"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 + +[node name="test1" type="Button" parent="VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Test 1" + +[node name="test2" type="Button" parent="VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Test 2" + +[node name="test3" type="Button" parent="VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Test 3" + +[node name="PanelContainer" type="TabContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +current_tab = 0 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="Panel1" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel1"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 14.0 +grow_horizontal = 2 +text = "Panel 1" + +[node name="Panel2" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel2"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 14.0 +grow_horizontal = 2 +text = "Panel 2" + +[node name="Panel3" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel3"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 14.0 +grow_horizontal = 2 +text = "Panel 3" + +[node name="Line2D" type="Line2D" parent="VBoxContainer"] +points = PackedVector2Array(0, 0, 20, 0) +width = 30.0 +default_color = Color(1, 0.0509804, 0.192157, 1) + +[node name="Line2D2" type="Line2D" parent="VBoxContainer"] +points = PackedVector2Array(20, 0, 40, 0) +width = 30.0 +default_color = Color(0.0392157, 1, 0.278431, 1) + +[node name="Line2D3" type="Line2D" parent="VBoxContainer"] +points = PackedVector2Array(40, 0, 60, 0) +width = 30.0 +default_color = Color(1, 0.0392157, 0.247059, 1) + +[node name="CanvasLayer" type="CanvasLayer" parent="."] + +[node name="Touch1" type="TouchScreenButton" parent="CanvasLayer"] +unique_name_in_owner = true +position = Vector2(643, 295) +texture_normal = ExtResource("2_xgglm") +texture_pressed = ExtResource("2_xgglm") + +[node name="Button" type="Button" parent="CanvasLayer"] +custom_minimum_size = Vector2(200, 80) +offset_left = 923.0 +offset_top = 529.0 +offset_right = 1123.0 +offset_bottom = 609.0 +text = "Exit" + +[node name="TextInput" type="LineEdit" parent="."] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 694.0 +offset_top = 189.0 +offset_right = 1020.0 +offset_bottom = 268.0 + +[connection signal="pressed" from="VBoxContainer/HBoxContainer/test1" to="." method="_on_test_pressed" binds= [1]] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/test2" to="." method="_on_test_pressed" binds= [2]] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/test3" to="." method="_on_test_pressed" binds= [3]] +[connection signal="pressed" from="CanvasLayer/Touch1" to="." method="_on_touch_1_pressed"] +[connection signal="released" from="CanvasLayer/Touch1" to="." method="_on_touch_1_released"] +[connection signal="pressed" from="CanvasLayer/Button" to="." method="_on_exit_pressed"] diff --git a/addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn b/addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn new file mode 100644 index 0000000..8bd646d --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn @@ -0,0 +1,67 @@ +[gd_scene format=3 uid="uid://bvp8uaof31fhm"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="test1" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Test 1" + +[node name="test2" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Test 2" + +[node name="test3" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Test 3" + +[node name="PanelContainer" type="TabContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"] +layout_mode = 2 + +[node name="Panel1" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +color = Color(0.694118, 0.207843, 0.207843, 1) + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel1"] +layout_mode = 0 +anchor_right = 1.0 +offset_bottom = 14.0 +text = "Panel 1" + +[node name="Panel2" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +color = Color(0.219608, 0.662745, 0.380392, 1) + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel2"] +layout_mode = 0 +anchor_right = 1.0 +offset_bottom = 14.0 +text = "Panel 2" + +[node name="Panel3" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +color = Color(0.12549, 0.286275, 0.776471, 1) + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel3"] +layout_mode = 0 +anchor_right = 1.0 +offset_bottom = 14.0 +text = "Panel 3" diff --git a/addons/gdUnit4/test/mocker/resources/snake_case.gd b/addons/gdUnit4/test/mocker/resources/snake_case.gd new file mode 100644 index 0000000..1c6183a --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/snake_case.gd @@ -0,0 +1,4 @@ +extends RefCounted + +func custom_func() -> void: + pass diff --git a/addons/gdUnit4/test/mocker/resources/snake_case.gd.uid b/addons/gdUnit4/test/mocker/resources/snake_case.gd.uid new file mode 100644 index 0000000..90cf1e9 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/snake_case.gd.uid @@ -0,0 +1 @@ +uid://bkuos52h3attk diff --git a/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd b/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd new file mode 100644 index 0000000..28073ae --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd @@ -0,0 +1,6 @@ +class_name snake_case_class_name +extends RefCounted + + +func custom_func() -> void: + pass diff --git a/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd.uid b/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd.uid new file mode 100644 index 0000000..387a7b8 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd.uid @@ -0,0 +1 @@ +uid://chd6pn8pywk3v diff --git a/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd b/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd new file mode 100644 index 0000000..de7b171 --- /dev/null +++ b/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd @@ -0,0 +1,74 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GodotGdErrorMonitorTest +extends GdUnitTestSuite + + +func before() -> void: + # disable default error reporting for testing + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, false) + + +func test_monitor_push_error() -> void: + var monitor := GodotGdErrorMonitor.new() + monitor._logger._is_report_push_errors = true + # no errors reported + monitor.start() + monitor.stop() + assert_array(monitor.to_reports()).is_empty() + + # push error + monitor.start() + force_push_error() + monitor.stop() + + var reports := monitor.to_reports() + assert_array(reports).has_size(1) + var report := reports[0] + assert_str(report.message()) \ + .contains("Test GodotGdErrorMonitor 'push_error' reporting") + assert_object(report.stack_trace()) \ + .is_equal(GdUnitStackTrace.new([ + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd", 74, "force_push_error2"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd", 69, "force_push_error"), + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd", 24, "test_monitor_push_error") + ])) + assert_int(report.line_number()).is_equal(74) + + +func test_monitor_push_waring() -> void: + var monitor := GodotGdErrorMonitor.new() + monitor._logger._is_report_push_errors = true + + # push error + monitor.start() + push_warning("Test GodotGdErrorMonitor 'push_warning' reporting") + monitor.stop() + + var reports := monitor.to_reports() + assert_array(reports).has_size(1) + var report := reports[0] + assert_str(report.message())\ + .contains("Test GodotGdErrorMonitor 'push_warning' reporting") + assert_object(report.stack_trace()) \ + .is_equal(GdUnitStackTrace.new([ + GdUnitStackTraceElement.new("res://addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd", 47, "test_monitor_push_waring") + ])) + assert_int(report.line_number()).is_equal(47) + + +func test_fail_by_push_error(_do_skip := true, _skip_reason := "disabled to not produce errors, enable only for direct testing") -> void: + GdUnitThreadManager.get_current_context().get_execution_context().error_monitor._logger._is_report_push_errors = true + push_error("test error") + + +func force_push_error() -> void: + @warning_ignore("redundant_await") + await force_push_error2() + + +func force_push_error2() -> void: + #await get_tree().process_frame + push_error("Test GodotGdErrorMonitor 'push_error' reporting") diff --git a/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd.uid b/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd.uid new file mode 100644 index 0000000..d793f74 --- /dev/null +++ b/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd.uid @@ -0,0 +1 @@ +uid://byifhq5aljc6s diff --git a/addons/gdUnit4/test/network/GdUnitTcpServerIntegrationTest.gd b/addons/gdUnit4/test/network/GdUnitTcpServerIntegrationTest.gd new file mode 100644 index 0000000..8b24cac --- /dev/null +++ b/addons/gdUnit4/test/network/GdUnitTcpServerIntegrationTest.gd @@ -0,0 +1,100 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/network/GdUnitTcpServer.gd' + +var tcp_server: GdUnitTcpServer +var tcp_client: GdUnitTcpClient + + +## We start a custom test server for this suite +func before() -> void: + tcp_server = GdUnitTcpServer.new("Test TCP Server") + tcp_client = GdUnitTcpClient.new("Test TCP Client", true) + add_child(tcp_server) + add_child(tcp_client) + + var result := tcp_server.start(62222) + if not result.is_success(): + return + + var server_port: int = result.value() + tcp_client.start("127.0.0.1", server_port) + + # wait until the client is connected + for n in 200: + await await_idle_frame() + if tcp_client.is_client_connected(): + break + + +## Shutdown the test server +func after() -> void: + tcp_client.stop() + tcp_client.queue_free() + await await_idle_frame() + tcp_server.stop() + tcp_server.queue_free() + await await_idle_frame() + + +func test_receive_single_message() -> void: + var signal_collector_ := signal_collector(tcp_server) + await await_idle_frame() + + # send a single test message + tcp_client.send(RPCMessage.of("Test Message")) + await await_idle_frame() + + # expect the RPCMessage is received and emitted + assert_bool(signal_collector_.is_emitted("rpc_data", [RPCMessage.of("Test Message")])).is_true() + + +func test_receive_multy_message() -> void: + var signal_collector_ := signal_collector(tcp_server) + await await_idle_frame() + + # send a two test message + tcp_client.send(RPCMessage.of("Test Message A")) + tcp_client.send(RPCMessage.of("Test Message B")) + await await_idle_frame() + + # expect the RPCMessage is received and emitted + assert_bool(signal_collector_.is_emitted("rpc_data", [RPCMessage.of("Test Message A")])).is_true() + assert_bool(signal_collector_.is_emitted("rpc_data", [RPCMessage.of("Test Message B")])).is_true() + + +func add_data(package: StreamPeerBuffer, rpc_data: RPC) -> int: + var buffer := rpc_data.serialize().to_utf8_buffer() + var package_size := buffer.size() + package.put_u32(0xDEADBEEF) + package.put_u32(buffer.size()) + package.put_data(buffer) + return package_size + + + +# TODO refactor out and provide as public interface to can be reuse on other tests +class TestGdUnitSignalCollector: + var _signalCollector: GdUnitSignalCollector + var _emitter: Object + + + func _init(emitter: Object) -> void: + _emitter = emitter + _signalCollector = GdUnitSignalCollector.new() + _signalCollector.register_emitter(emitter) + + + func is_emitted(signal_name: String, expected_args: Array) -> bool: + return _signalCollector.match(_emitter, signal_name, expected_args) + + + func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + _signalCollector.unregister_emitter(_emitter) + + +func signal_collector(instance: Object) -> TestGdUnitSignalCollector: + return TestGdUnitSignalCollector.new(instance) diff --git a/addons/gdUnit4/test/network/GdUnitTcpServerIntegrationTest.gd.uid b/addons/gdUnit4/test/network/GdUnitTcpServerIntegrationTest.gd.uid new file mode 100644 index 0000000..5f44d3c --- /dev/null +++ b/addons/gdUnit4/test/network/GdUnitTcpServerIntegrationTest.gd.uid @@ -0,0 +1 @@ +uid://bx5lbugg2dq2q diff --git a/addons/gdUnit4/test/reporters/GdUnitConsoleTestReporterTest.gd b/addons/gdUnit4/test/reporters/GdUnitConsoleTestReporterTest.gd new file mode 100644 index 0000000..0fb8c56 --- /dev/null +++ b/addons/gdUnit4/test/reporters/GdUnitConsoleTestReporterTest.gd @@ -0,0 +1,59 @@ +# GdUnit generated TestSuite +class_name GdUnitConsoleTestReporterTest +extends GdUnitTestSuite + + +var reporter := GdUnitConsoleTestReporter.new(GdUnitCSIMessageWriter.new()) + + +func before_test() -> void: + reporter.test_session = GdUnitTestSession.new([], "res://reports") + reporter.on_gdunit_event(GdUnitInit.new()) + + +func test_on_gdunit_event_init() -> void: + assert_int(reporter.processed_suite_count()).is_equal(0) + assert_int(reporter.total_test_count()).is_equal(0) + assert_int(reporter.total_flaky_count()).is_equal(0) + assert_int(reporter.total_error_count()).is_equal(0) + assert_int(reporter.total_failure_count()).is_equal(0) + assert_int(reporter.total_skipped_count()).is_equal(0) + assert_int(reporter.total_orphan_count()).is_equal(0) + assert_int(reporter.elapsed_time()).is_equal(0) + + +func test_on_gdunit_event_empty_test_suite() -> void: + reporter.on_gdunit_event(GdUnitEvent.new().suite_before("res://tests/suite_a.gd", "suide_a", 0)) + reporter.on_gdunit_event(GdUnitEvent.new().suite_after("res://tests/suite_a.gd", "suide_a")) + + assert_int(reporter.processed_suite_count()).is_equal(1) + assert_int(reporter.total_test_count()).is_equal(0) + assert_int(reporter.total_flaky_count()).is_equal(0) + assert_int(reporter.total_error_count()).is_equal(0) + assert_int(reporter.total_failure_count()).is_equal(0) + assert_int(reporter.total_skipped_count()).is_equal(0) + assert_int(reporter.total_orphan_count()).is_equal(0) + assert_int(reporter.elapsed_time()).is_equal(0) + + +func test_on_gdunit_event_full_test_suite() -> void: + var test_id_a := GdUnitGUID.new() + var test_id_b := GdUnitGUID.new() + var test_id_c := GdUnitGUID.new() + reporter.on_gdunit_event(GdUnitEvent.new().suite_before("res://tests/suite_a.gd", "suide_a", 0)) + reporter.on_gdunit_event(GdUnitEvent.new().test_before(test_id_a)) + reporter.on_gdunit_event(GdUnitEvent.new().test_after(test_id_a, "test_a")) + reporter.on_gdunit_event(GdUnitEvent.new().test_before(test_id_b)) + reporter.on_gdunit_event(GdUnitEvent.new().test_after(test_id_b, "test_b")) + reporter.on_gdunit_event(GdUnitEvent.new().test_before(test_id_c)) + reporter.on_gdunit_event(GdUnitEvent.new().test_after(test_id_c, "test_c")) + reporter.on_gdunit_event(GdUnitEvent.new().suite_after("res://tests/suite_a.gd", "suide_a")) + + assert_int(reporter.processed_suite_count()).is_equal(1) + assert_int(reporter.total_test_count()).is_equal(3) + assert_int(reporter.total_flaky_count()).is_equal(0) + assert_int(reporter.total_error_count()).is_equal(0) + assert_int(reporter.total_failure_count()).is_equal(0) + assert_int(reporter.total_skipped_count()).is_equal(0) + assert_int(reporter.total_orphan_count()).is_equal(0) + assert_int(reporter.elapsed_time()).is_equal(0) diff --git a/addons/gdUnit4/test/reporters/GdUnitConsoleTestReporterTest.gd.uid b/addons/gdUnit4/test/reporters/GdUnitConsoleTestReporterTest.gd.uid new file mode 100644 index 0000000..4442654 --- /dev/null +++ b/addons/gdUnit4/test/reporters/GdUnitConsoleTestReporterTest.gd.uid @@ -0,0 +1 @@ +uid://s014asutkcjy diff --git a/addons/gdUnit4/test/reporters/GdUnitReportWriterIntegrationTest.gd b/addons/gdUnit4/test/reporters/GdUnitReportWriterIntegrationTest.gd new file mode 100644 index 0000000..4c8656e --- /dev/null +++ b/addons/gdUnit4/test/reporters/GdUnitReportWriterIntegrationTest.gd @@ -0,0 +1,128 @@ +class_name GdUnitReportWriterIntegrationTest +extends GdUnitTestSuite + + +const RESOURCE_REPORTS := "res://addons/gdUnit4/test/reporters/resources/" +const RESOURCE_SUITES := "res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/" + +var _report_dir: String +var _reporter := GdUnitTestReporter.new() +var _report_summary: GdUnitReportSummary +var _report_writer: GdUnitReportWriter +var _test_session: GdUnitTestSession + + +#region test hooks +func _setup_base(report_dir_name: String, writer: GdUnitReportWriter) -> void: + _report_dir = GdUnitFileAccess.create_temp_dir(report_dir_name) + _report_writer = writer + var formatter := func(s: String) -> String: return s + _report_summary = GdUnitReportSummary.new(formatter) + _reporter.init_summary() + GdUnitSignals.instance().gdunit_event_debug.connect(_on_test_event) + ProjectSettings.set_setting(GdUnitSettings.TEST_FLAKY_CHECK, false) + + +func after() -> void: + GdUnitSignals.instance().gdunit_event_debug.disconnect(_on_test_event) +#endregion + + +func _on_test_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.TESTSUITE_BEFORE: + _reporter.init_statistics() + _report_summary.add_testsuite_report(event.resource_path(), event.suite_name(), event.total_count()) + GdUnitEvent.TESTSUITE_AFTER: + var statistics := _reporter.build_test_suite_statisitcs(event) + _report_summary.update_testsuite_counters( + event.resource_path(), + _reporter.error_count(statistics), + _reporter.failed_count(statistics), + _reporter.orphan_nodes(statistics), + _reporter.skipped_count(statistics), + _reporter.flaky_count(statistics), + event.elapsed_time()) + _report_summary.add_testsuite_reports( + event.resource_path(), + event.reports() + ) + GdUnitEvent.TESTCASE_BEFORE: + var test := _test_session.find_test_by_id(event.guid()) + _report_summary.add_testcase(test.source_file, test.suite_name, test.display_name) + GdUnitEvent.TESTCASE_AFTER: + _reporter.add_test_statistics(event) + var test := _test_session.find_test_by_id(event.guid()) + _report_summary.set_counters(test.source_file, + test.display_name, + event.error_count(), + event.failed_count(), + event.orphan_nodes(), + event.is_skipped(), + event.is_flaky(), + event.elapsed_time()) + _report_summary.add_reports(test.source_file, test.display_name, event.reports()) + + +func run_tests(tests: Array[GdUnitTestCase], settings := {}) -> void: + await GdUnitThreadManager.run("test_executor_%d" % randi(), func() -> void: + var executor := GdUnitTestSuiteExecutor.new(true) + + var saves_settings := {} + for key: String in settings.keys(): + saves_settings[key] = ProjectSettings.get_setting(key) + ProjectSettings.set_setting(key, settings[key]) + + await (Engine.get_main_loop() as SceneTree).process_frame + await executor.run_and_wait(tests) + + for key: String in saves_settings.keys(): + ProjectSettings.set_setting(key, saves_settings[key]) + ) + + +func _load_test_cases(suite_resource_path: String) -> Array[GdUnitTestCase]: + var suite_failing_tests := GdUnitTestResourceLoader.load_tests(RESOURCE_SUITES + suite_resource_path) + return Array(suite_failing_tests.values(), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + +func _patch_out_timings(content: String) -> String: + return content + + +#region test utils +func _replace_latency_values(input_text: String) -> String: + var regex := RegEx.new() + regex.compile("\\d{1,3}+ms") + return regex.sub(input_text, "999ms", true) + + +func _replace_all_timestamps(input_text: String) -> String: + var regex := RegEx.new() + regex.compile("\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}") + return regex.sub(input_text, "2026-04-22 18:27:14", true) + + +func _replace_time_values(input_text: String) -> String: + var regex := RegEx.new() + regex.compile('time="[\\d.]+"') + return regex.sub(input_text, 'time="0.000"', true) + + +func _replace_date_ids(input_text: String) -> String: + var regex := RegEx.new() + regex.compile('id="\\d{4}-\\d{2}-\\d{2}"') + return regex.sub(input_text, 'id="2026-01-01"', true) + + +func _replace_timestamps(input_text: String) -> String: + var regex := RegEx.new() + regex.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}") + return regex.sub(input_text, "2026-01-01T00:00:00", true) +#endregion + + +func _assert_report_matches(actual_path: String, expected_path: String) -> void: + var actual := _patch_out_timings(FileAccess.open(actual_path, FileAccess.READ).get_as_text()) + var expected := _patch_out_timings(FileAccess.open(expected_path, FileAccess.READ).get_as_text()) + assert_str(actual).is_equal(expected) diff --git a/addons/gdUnit4/test/reporters/GdUnitReportWriterIntegrationTest.gd.uid b/addons/gdUnit4/test/reporters/GdUnitReportWriterIntegrationTest.gd.uid new file mode 100644 index 0000000..8b15f60 --- /dev/null +++ b/addons/gdUnit4/test/reporters/GdUnitReportWriterIntegrationTest.gd.uid @@ -0,0 +1 @@ +uid://kcd1syx8kgds diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlEncoderTest.gd b/addons/gdUnit4/test/reporters/html/GdUnitHtmlEncoderTest.gd new file mode 100644 index 0000000..0327ba6 --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlEncoderTest.gd @@ -0,0 +1,53 @@ +class_name GdUnitHtmlEncoderTest +extends GdUnitTestSuite + + +#region plain text +func test_plain_text_is_unchanged() -> void: + assert_str(GdUnitHtmlEncoder.encode("hello world")).is_equal("hello world") + + +func test_empty_string_is_unchanged() -> void: + assert_str(GdUnitHtmlEncoder.encode("")).is_equal("") +#endregion + + +#region ampersand +func test_ampersand_is_encoded() -> void: + assert_str(GdUnitHtmlEncoder.encode("a & b")).is_equal("a & b") + + +func test_ampersand_is_not_double_encoded() -> void: + assert_str(GdUnitHtmlEncoder.encode("&")).is_equal("&amp;") +#endregion + + +#region less-than / greater-than +func test_less_than_is_encoded() -> void: + assert_str(GdUnitHtmlEncoder.encode("Array")).is_equal("Array<int>") + + +func test_greater_than_is_encoded() -> void: + assert_str(GdUnitHtmlEncoder.encode("x > 0")).is_equal("x > 0") +#endregion + + +#region quotes +func test_double_quote_is_encoded() -> void: + assert_str(GdUnitHtmlEncoder.encode('say "hi"')).is_equal("say "hi"") + + +func test_single_quote_is_encoded() -> void: + assert_str(GdUnitHtmlEncoder.encode("it's")).is_equal("it's") +#endregion + + +#region mixed +func test_all_special_chars_in_one_string() -> void: + assert_str(GdUnitHtmlEncoder.encode("a & b")) \ + .is_equal("<a href='x' title="y">a & b</a>") + + +func test_multiline_text_is_preserved() -> void: + assert_str(GdUnitHtmlEncoder.encode("line1\nline2")).is_equal("line1\nline2") +#endregion diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlEncoderTest.gd.uid b/addons/gdUnit4/test/reporters/html/GdUnitHtmlEncoderTest.gd.uid new file mode 100644 index 0000000..c9d3cb1 --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlEncoderTest.gd.uid @@ -0,0 +1 @@ +uid://dopb4lrmd7cdf diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlPatternsTest.gd b/addons/gdUnit4/test/reporters/html/GdUnitHtmlPatternsTest.gd new file mode 100644 index 0000000..05cb4d6 --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlPatternsTest.gd @@ -0,0 +1,90 @@ +# GdUnit generated TestSuite +class_name GdUnitHtmlPatternsTest +extends GdUnitTestSuite + + +func _make_suite_report(resource_path: String, suite_name: String) -> GdUnitTestSuiteReport: + return GdUnitTestSuiteReport.new(resource_path, suite_name, 1, func(s: String) -> String: return s) + + +#region create_suite_record +func test_href_is_quoted() -> void: + var report := _make_suite_report("res://test/game_test.gd", "game_test") + var html := GdUnitHtmlPatterns.create_suite_record("./test_suites/game_test.html", report) + assert_str(html).contains('href="./test_suites/game_test.html"') + + +func test_href_is_quoted_when_link_contains_spaces() -> void: + var report := _make_suite_report("res://my test/game_test.gd", "game_test") + var link := "./test_suites/my_test.game_test.html" + var html := GdUnitHtmlPatterns.create_suite_record(link, report) + assert_str(html).contains('href="%s"' % link) + + +func test_href_is_not_unquoted() -> void: + var report := _make_suite_report("res://my test/game_test.gd", "game_test") + var link := "./test_suites/my_test.game_test.html" + var html := GdUnitHtmlPatterns.create_suite_record(link, report) + assert_str(html).not_contains("href=%s" % link) +#endregion + + +#region get_path_as_link +func test_get_path_as_link_simple() -> void: + var report := _make_suite_report("res://test/suite/game_test.gd", "game_test") + assert_str(GdUnitHtmlPatterns.get_path_as_link(report)).is_equal("../path/test.suite.html") + + +func test_get_path_as_link_converts_slashes_to_dots() -> void: + var report := _make_suite_report("res://a/b/c/game_test.gd", "game_test") + assert_str(GdUnitHtmlPatterns.get_path_as_link(report)).is_equal("../path/a.b.c.html") +#endregion + + +#region create_suite_output_path +func test_create_suite_output_path_converts_slashes_to_dots() -> void: + var result := GdUnitHtmlPatterns.create_suite_output_path("/reports", "test/suite", "MyTest") + assert_str(result).is_equal("/reports/test_suites/test.suite.MyTest.html") + + +func test_create_suite_output_path_replaces_spaces_with_underscores() -> void: + var result := GdUnitHtmlPatterns.create_suite_output_path("/reports", "my test/suite", "MyTest") + assert_str(result).is_equal("/reports/test_suites/my_test.suite.MyTest.html") +#endregion + + +#region create_path_output_path +func test_create_path_output_path_converts_slashes_to_dots() -> void: + var result := GdUnitHtmlPatterns.create_path_output_path("/reports", "test/suite") + assert_str(result).is_equal("/reports/path/test.suite.html") + + +func test_create_path_output_path_replaces_spaces_with_underscores() -> void: + var result := GdUnitHtmlPatterns.create_path_output_path("/reports", "my test/suite") + assert_str(result).is_equal("/reports/path/my_test.suite.html") +#endregion + + +#region write_html_file +func test_write_html_file_creates_file_with_content() -> void: + var dir := GdUnitFileAccess.create_temp_dir("html_patterns_test/write_content") + var output_path := dir + "/output.html" + GdUnitHtmlPatterns.write_html_file(output_path, "test") + assert_bool(FileAccess.file_exists(output_path)).is_true() + assert_str(FileAccess.open(output_path, FileAccess.READ).get_as_text()).is_equal("test") + + +func test_write_html_file_creates_missing_directory() -> void: + var dir := GdUnitFileAccess.create_temp_dir("html_patterns_test") + var output_path := dir + "/new_subdir/output.html" + GdUnitHtmlPatterns.write_html_file(output_path, "") + assert_bool(FileAccess.file_exists(output_path)).is_true() + + +func test_write_html_file_overwrites_existing_content() -> void: + var dir := GdUnitFileAccess.create_temp_dir("html_patterns_test/overwrite") + var output_path := dir + "/output.html" + GdUnitHtmlPatterns.write_html_file(output_path, "first") + GdUnitHtmlPatterns.write_html_file(output_path, "second") + assert_str(FileAccess.open(output_path, FileAccess.READ).get_as_text()).is_equal("second") +#endregion diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlPatternsTest.gd.uid b/addons/gdUnit4/test/reporters/html/GdUnitHtmlPatternsTest.gd.uid new file mode 100644 index 0000000..6fcc87e --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlPatternsTest.gd.uid @@ -0,0 +1 @@ +uid://cwvqdkcl8o8k1 diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterIntegrationTest.gd b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterIntegrationTest.gd new file mode 100644 index 0000000..24bed23 --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterIntegrationTest.gd @@ -0,0 +1,43 @@ +class_name GdUnitHtmlReportWriterIntegrationTest +extends GdUnitReportWriterIntegrationTest + + +func before() -> void: + _setup_base("html_reports/report_1", GdUnitHtmlReportWriter.new()) + + +func _patch_out_timings(content: String) -> String: + return _replace_all_timestamps(_replace_latency_values(content)) + + +func test_write_report() -> void: + var tests: Array[GdUnitTestCase] = [] + tests.append_array(_load_test_cases("TestSuiteAllStagesSuccess.resource")) + tests.append_array(_load_test_cases("TestSuiteFailOnMultipeStages.resource")) + tests.append_array(_load_test_cases("TestCaseSkipped.resource")) + _test_session = GdUnitTestSession.new(tests, _report_dir) + await run_tests(tests) + + _report_writer.write(_test_session.report_path, _report_summary) + + var suite_report_path := "addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces" + _assert_report_matches( + _report_dir + "/index.html", + RESOURCE_REPORTS + "report_1/index.html" + ) + _assert_report_matches( + _report_dir + "/path/%s.html" % suite_report_path, + RESOURCE_REPORTS + "report_1/path/%s.html" % suite_report_path + ) + _assert_report_matches( + _report_dir + "/test_suites/%s.TestSuiteAllStagesSuccess.html" % suite_report_path, + RESOURCE_REPORTS + "report_1/test_suites/%s.TestSuiteAllStagesSuccess.html" % suite_report_path + ) + _assert_report_matches( + _report_dir + "/test_suites/%s.TestSuiteFailOnMultipeStages.html" % suite_report_path, + RESOURCE_REPORTS + "report_1/test_suites/%s.TestSuiteFailOnMultipeStages.html" % suite_report_path + ) + _assert_report_matches( + _report_dir + "/test_suites/%s.TestCaseSkipped.html" % suite_report_path, + RESOURCE_REPORTS + "report_1/test_suites/%s.TestCaseSkipped.html" % suite_report_path + ) diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterIntegrationTest.gd.uid b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterIntegrationTest.gd.uid new file mode 100644 index 0000000..b0cb3e1 --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterIntegrationTest.gd.uid @@ -0,0 +1 @@ +uid://mrnlcjsphbtu diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterTest.gd b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterTest.gd new file mode 100644 index 0000000..bed53fa --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterTest.gd @@ -0,0 +1,40 @@ +# GdUnit generated TestSuite +class_name GdUnitHtmlReportWriterTest +extends GdUnitTestSuite + + +const SNAPSHOT_DIR := "res://addons/gdUnit4/test/reporters/html/resources" + +var _report_dir: String + + +func before() -> void: + _report_dir = GdUnitFileAccess.create_temp_dir("html_report_writer_test") + var formatter := func(s: String) -> String: return s + var summary := GdUnitReportSummary.new(formatter) + summary.add_testsuite_report("res://my test suite/SomeTest.gd", "SomeTest", 1) + GdUnitHtmlReportWriter.new().write(_report_dir, summary) + + +#region space replacement in generated hrefs +func test_suite_href_in_index_replaces_spaces_with_underscores() -> void: + var index_html := FileAccess.open(_report_dir + "/index.html", FileAccess.READ).get_as_text() + assert_str(index_html).contains('href="./test_suites/my_test_suite.SomeTest.html"') + + +func test_path_href_in_index_replaces_spaces_with_underscores() -> void: + var index_html := FileAccess.open(_report_dir + "/index.html", FileAccess.READ).get_as_text() + assert_str(index_html).contains('href="./path/my_test_suite.html"') + + +func test_breadcrumb_href_in_suite_report_replaces_spaces_with_underscores() -> void: + var suite_html := FileAccess.open( + _report_dir + "/test_suites/my_test_suite.SomeTest.html", FileAccess.READ + ).get_as_text() + assert_str(suite_html).contains('href="../path/my_test_suite.html"') + + +func test_suite_file_uses_underscores_not_spaces() -> void: + assert_bool(FileAccess.file_exists(_report_dir + "/test_suites/my_test_suite.SomeTest.html")).is_true() + assert_bool(FileAccess.file_exists(_report_dir + "/test_suites/my test suite.SomeTest.html")).is_false() +#endregion diff --git a/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterTest.gd.uid b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterTest.gd.uid new file mode 100644 index 0000000..cd8e930 --- /dev/null +++ b/addons/gdUnit4/test/reporters/html/GdUnitHtmlReportWriterTest.gd.uid @@ -0,0 +1 @@ +uid://bmbe14xghgmg1 diff --git a/addons/gdUnit4/test/reporters/resources/.gdignore b/addons/gdUnit4/test/reporters/resources/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/reporters/resources/report_1/css/breadcrumb.css b/addons/gdUnit4/test/reporters/resources/report_1/css/breadcrumb.css new file mode 100644 index 0000000..17215ff --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/css/breadcrumb.css @@ -0,0 +1,66 @@ +.breadcrumb { + display: flex; + border-radius: 6px; + overflow: hidden; + height: 45px; + z-index: 1; + background-color: #9d73eb; + margin-top: 0px; + margin-bottom: 10px; + box-shadow: 0 0 3px black; +} + +.breadcrumb a { + position: relative; + display: flex; + -ms-flex-positive: 1; + flex-grow: 1; + text-decoration: none; + margin: auto; + height: 100%; + color: white; +} + +.breadcrumb a:first-child { + padding-left: 5.2px; +} + +.breadcrumb a:last-child { + padding-right: 5.2px; +} + +.breadcrumb a:after { + content: ""; + position: absolute; + display: inline-block; + width: 45px; + height: 45px; + top: 0; + right: -20px; + background-color: #9d73eb; + border-top-right-radius: 5px; + transform: scale(0.707) rotate(45deg); + box-shadow: 2px -2px rgba(0, 0, 0, 0.25); + z-index: 1; +} + +.breadcrumb a:last-child:after { + content: none; +} + +.breadcrumb a.active, +.breadcrumb a:hover { + background: #b899f2; + color: white; + text-decoration: underline; +} + +.breadcrumb a.active:after, +.breadcrumb a:hover:after { + background: #b899f2; +} + +.breadcrumb span { + margin: inherit; + z-index: 2; +} diff --git a/addons/gdUnit4/test/reporters/resources/report_1/css/logo.png b/addons/gdUnit4/test/reporters/resources/report_1/css/logo.png new file mode 100644 index 0000000..12de79f Binary files /dev/null and b/addons/gdUnit4/test/reporters/resources/report_1/css/logo.png differ diff --git a/addons/gdUnit4/test/reporters/resources/report_1/css/styles.css b/addons/gdUnit4/test/reporters/resources/report_1/css/styles.css new file mode 100644 index 0000000..e92d59b --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/css/styles.css @@ -0,0 +1,475 @@ +html, +body { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + font-family: sans-serif; + background-color: white; + height: 100%; +} + +main { + flex-grow: 1; + overflow: auto; + margin: 0 10em; +} + + +header { + color: white; + padding: 1px; + position: relative; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); +} + +.logo { + position: fixed; + top: 20px; + left: 20px; + display: flex; + align-items: center; + z-index: 1000; + filter: grayscale(1); + mix-blend-mode: plus-lighter; +} + +.logo img { + width: 64px; + height: 64px; +} + +.logo span { + font-size: 1.2em; + color: lightslategray; +} + +.report-container { + margin: 0 15em; + text-align: center; + margin-top: 60px; + flex-grow: 0; +} + +h1 { + margin: 0 0 20px 0; + font-size: 2.5em; + font-weight: normal; +} + +.summary { + display: inline-flex; + justify-content: center; + flex-wrap: nowrap; + margin-bottom: 20px; + align-items: baseline; + max-width: 960px; +} + +.summary-item { + flex: 1; + min-width: 80px; +} + +.label { + font-size: 1em; + flex-wrap: nowrap; +} + +.value { + font-size: 0.9em; + display: block; + padding-top: 10px; + color: lightgray; +} + +.success-rate { + padding-left: 40px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.check-icon { + background-color: #34c538; + color: white; + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4em; +} + +.rate-text { + text-align: center; + flex-wrap: nowrap; +} + +.percentage { + font-size: 1.2em; + font-weight: bold; +} + + +nav { + padding: 20px 0px; + font-family: monospace; +} + +nav ul { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + justify-content: flex-start; + border-bottom: 1px solid lightgray; +} + +nav li { + cursor: pointer; + padding: 5px 20px; + font-size: 1.1em; + color: lightslategray; +} + +nav li.active { + color: darkslategray; + border-bottom: 1px solid darkslategray; + font-weight: bold; +} + +div#content { + height: calc(100vh - 400px); +} + + +table { + width: 100%; + height: 100%; + border-collapse: collapse; + overflow: hidden; +} + +thead th { + position: sticky; + top: 0; + background-color: white; + z-index: 1; + border-bottom: 2px solid #ddd; +} + +tbody { + display: block; + /* Limit the height of the table body */ + max-height: calc(100vh - 400px); + /* Enable scrolling on the table body */ + overflow-y: auto; +} + +thead, +tbody tr { + display: table; + width: 100%; + table-layout: fixed; +} + +tbody td { + overflow: hidden; +} + +/* Ensure scrollbar visibility */ +tbody::-webkit-scrollbar { + height: 4px; + width: 14px; +} + +tbody::-webkit-scrollbar-thumb { + background-color: #aaa6a6; + border-radius: 4px; +} + +tbody::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +th, +td { + font-size: .9em; + padding: 5px 0px; + border-bottom: 1px solid #eee; + color: lightslategrey; + text-align: left; + text-wrap: nowrap; + /* Default max and min width for all columns */ + max-width: 150px; + min-width: 80px; + width: 80px; +} + +th { + font-size: 1em; + font-weight: normal; + padding-top: 20px; + color: gray; + text-wrap: nowrap; +} + +.tab-report { + display: grid; + grid-template-columns: 100%; + margin-bottom: 20px; +} + +.tab-report-grid { + display: grid; + grid-template-columns: 70% 30%; + margin-bottom: 20px; +} + + +/* Specific styling for the first column (Testcase) */ +th:first-child, +td:first-child { + padding-left: 5px; + text-align: left; + /* Max width for the first column */ + min-width: 249px; + width: 250px; + /* Enable scrollbar if content exceeds max-width */ + white-space: nowrap; + overflow: auto; +} + +/* Scrollbar styles for first column */ +td:first-child { + overflow-x: auto; + text-overflow: initial; +} + +/* Scrollbar appearance */ +td:first-child::-webkit-scrollbar { + height: 6px; +} + +td:first-child::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 10px; +} + +td:first-child::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +/* Max width for Result column */ +th:nth-child(2), +td:nth-child(2) { + max-width: 140px; + min-width: 140px; + width: 140px; +} + +/* Max width for Quick Results column */ +th:nth-child(9), +td:nth-child(9) { + max-width: 140px; + min-width: 140px; + width: 140px; + padding-right: 10px; +} + +/* Background color for alternating groups */ +.group-bg-1 { + background-color: #f1f1f1; +} + +.group-bg-2 { + background-color: #e0e0e0; +} + +.grid-item { + overflow: auto; + padding-left: 20px; + color: lightslategrey; + max-height: calc(100vh - 350px); +} + +div.tab td.report-column, +th.report-column { + display: none; +} + +/* Result status styles */ +.status { + padding: 2px 40px; + border-radius: 6px; + color: black; + width: 40px; + display: flex; + align-content: center; + align-items: center; +} + +.status-bar { + display: flex; + border-radius: 8px; + overflow: hidden; + height: 20px; + flex-wrap: nowrap; + justify-content: space-evenly; +} + +.status-bar-column { + margin: -2px; + color: black; + display: flex; + align-content: center; + align-items: center; + transition: width 0.3s ease; +} + +.status-skipped { + background-color: #888888; +} + +.status-passed { + background-color: #63bb38; +} + +.status-error { + background-color: #fd1100; +} + +.status-failed { + background-color: #ed594f; +} + +.status-flaky { + background-color: #1d9a1f; +} + +.status-warning { + background-color: #fdda3f; +} + +div.tab tr:hover { + background-color: #d9e7fa; + box-shadow: 0 0 5px black; +} + +div.tab tr.selected { + background-color: #d9e7fa; +} + +div.report-column { + margin-top: 10px; + width: 100%; + text-align: left; +} + +.logging-container { + width: 100%; + height: 100%; +} + +div.godot-report-frame { + margin: 10px; + font-family: monospace; + height: 100%; + background-color: #eee; +} + +div.include-footer { + position: fixed; + bottom: 0; + width: 100%; + display: flex; +} + +footer { + position: static; + left: 0; + bottom: 0; + width: 100%; + white-space: nowrap; + color: lightgray; + font-size: 12px; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); + display: flex; + justify-content: space-between; + align-items: center; +} + +footer p { + padding-left: 10em; +} + +footer .status-legend { + display: flex; + gap: 15px; + width: 500px; +} + +footer a { + color: lightgray; +} + +footer a:hover { + color: whitesmoke; +} + +footer a:visited { + color: whitesmoke; +} + +.status-legend-item { + display: flex; + align-items: center; + gap: 5px; +} + +.status-box { + width: 15px; + height: 15px; + border-radius: 3px; + display: inline-block; +} + +/* Normal link */ +a { + color: lightslategrey; +} + +/* Link when hovered */ +a:hover { + color: #9d73eb; +} + +/* Visited link */ +a:visited { + color: #8058e3; +} + +/* Active link (while being clicked) */ +a:active { + color: #8058e3; + /* Custom color when link is clicked */ +} + + +@media (max-width: 1024px) { + .summary { + flex-direction: column; + } + + nav ul { + flex-wrap: wrap; + } + + nav li { + margin-right: 10px; + margin-bottom: 5px; + } +} diff --git a/addons/gdUnit4/test/reporters/resources/report_1/index.html b/addons/gdUnit4/test/reporters/resources/report_1/index.html new file mode 100644 index 0000000..6f1d835 --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/index.html @@ -0,0 +1,250 @@ + + + + + + + GdUnit4 Report + + + + +
+ +
+

Summary Report

+
+
+ Test Suites + 3 +
+
+ Tests + 6 +
+
+ Skipped + 1 +
+
+ Flaky + 0 +
+
+ Failures + 5 +
+
+ Orphans + 0 +
+
+ Duration + 0ms +
+
+
โœ“
+
+ Success Rate + 16% +
+
+
+
+
+
+ +
+ +
+
+ +
+

Generated by GdUnit4 at 2026-05-16 07:27:22

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/test/reporters/resources/report_1/path/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.html b/addons/gdUnit4/test/reporters/resources/report_1/path/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.html new file mode 100644 index 0000000..c6cc715 --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/path/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.html @@ -0,0 +1,187 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + +
+ +
+

Report by Paths

+
+ res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/ +
+
+
+ TestSuites + 3 +
+
+ Tests + 6 +
+
+ Skipped + 1 +
+
+ Flaky + 0 +
+
+ Failures + 5 +
+
+ Orphans + 0 +
+
+ Duration + 30ms +
+
+
โœ“
+
+ Success Rate + 16% +
+
+
+
+ +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestSuitesResultTestsSkippedFlakyFailuresOrphansDurationSuccess rate
TestSuiteAllStagesSuccessPASSED2000011ms +
+
+
+
+
+
+
+
+
TestSuiteFailOnMultipeStagesFAILED2005010ms +
+
+
+
+
+
+
+
+
TestCaseSkippedSKIPPED210009ms +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+

Generated by GdUnit4 at 2026-05-16 07:27:22

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + diff --git a/addons/gdUnit4/test/reporters/resources/report_1/results.xml b/addons/gdUnit4/test/reporters/resources/report_1/results.xml new file mode 100644 index 0000000..cdf297a --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/results.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestCaseSkipped.html b/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestCaseSkipped.html new file mode 100644 index 0000000..d51abbf --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestCaseSkipped.html @@ -0,0 +1,208 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + + + + +
+ +
+

Testsuite Report

+
+ res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestCaseSkipped.resource +
+
+
+ Tests + 2 +
+
+ Skipped + 1 +
+
+ Flaky + 0 +
+
+ Failures + 0 +
+
+ Orphans + 0 +
+
+ Duration + 10ms +
+
+
โœ“
+
+ Success Rate + 100% +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestcaseResultSkippedOrphansDurationReport
test_case1SKIPPED100ms +
+[color=ff4500ff]This test is skipped![/color]
+  Reason: '[color=1e90ffff]do not run this[/color]'
+
+
+
+										
+
test_case2PASSED002ms +
+
+										
+
+
+
+

Failure Report

+
+
+
+
+ +
+

Generated by GdUnit4 at 2026-05-25 10:34:03

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestSuiteAllStagesSuccess.html b/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestSuiteAllStagesSuccess.html new file mode 100644 index 0000000..99404f5 --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestSuiteAllStagesSuccess.html @@ -0,0 +1,204 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + + + + +
+ +
+

Testsuite Report

+
+ res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteAllStagesSuccess.resource +
+
+
+ Tests + 2 +
+
+ Skipped + 0 +
+
+ Flaky + 0 +
+
+ Failures + 0 +
+
+ Orphans + 0 +
+
+ Duration + 11ms +
+
+
โœ“
+
+ Success Rate + 100% +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestcaseResultSkippedOrphansDurationReport
test_case1PASSED002ms +
+
+										
+
test_case2PASSED002ms +
+
+										
+
+
+
+

Failure Report

+
+
+
+
+ +
+

Generated by GdUnit4 at 2026-05-16 07:27:22

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestSuiteFailOnMultipeStages.html b/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestSuiteFailOnMultipeStages.html new file mode 100644 index 0000000..e17847e --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/report_1/test_suites/addons.gdUnit4.test.reporters.resources.suites.folder_with_spaces.TestSuiteFailOnMultipeStages.html @@ -0,0 +1,229 @@ + + + + + + + + + GdUnit4 Testsuite + + + + + + + + +
+ +
+

Testsuite Report

+
+ res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource +
+
+
+ Tests + 2 +
+
+ Skipped + 0 +
+
+ Flaky + 0 +
+
+ Failures + 5 +
+
+ Orphans + 0 +
+
+ Duration + 10ms +
+
+
โœ“
+
+ Success Rate + 0% +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestcaseResultSkippedOrphansDurationReport
TestSuite hooksn/a010ms +
+[color=green]line [/color][color=aqua]8:[/color] failed on after()
+										
+
test_case1FAILED003ms +
+failed on before_test()
+	at 'before_test' in res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource:11
+
+failed 1 on test_case1()
+	at 'test_case1' in res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource:17
+
+failed 2 on test_case1()
+	at 'test_case1' in res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource:18
+
+
+										
+
test_case2FAILED002ms +
+failed on before_test()
+	at 'before_test' in res://addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource:11
+
+
+										
+
+
+
+

Failure Report

+
+
+
+
+ +
+

Generated by GdUnit4 at 2026-05-16 07:27:22

+
+ + Skipped + + + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
+ + + + + diff --git a/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestCaseSkipped.resource b/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestCaseSkipped.resource new file mode 100644 index 0000000..f25f174 --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestCaseSkipped.resource @@ -0,0 +1,11 @@ +extends GdUnitTestSuite + + +@warning_ignore('unused_parameter') +func test_case1(timeout := 1000, do_skip:=1==1, skip_reason:="do not run this") -> void: + pass + + +@warning_ignore('unused_parameter') +func test_case2(skip_reason:="ignored") -> void: + pass diff --git a/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteAllStagesSuccess.resource b/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteAllStagesSuccess.resource new file mode 100644 index 0000000..93a8ce6 --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteAllStagesSuccess.resource @@ -0,0 +1,20 @@ +# this test suite ends with success, no failures or errors +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").is_equal("suite after") + +func before_test() -> void: + assert_str("test before").is_equal("test before") + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").is_equal("test_case1") + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource b/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource new file mode 100644 index 0000000..a9d68d1 --- /dev/null +++ b/addons/gdUnit4/test/reporters/resources/suites/folder with spaces/TestSuiteFailOnMultipeStages.resource @@ -0,0 +1,21 @@ +# this test suite fails on multiple stages +extends GdUnitTestSuite + +func before() -> void: + assert_str("suite before").is_equal("suite before") + +func after() -> void: + assert_str("suite after").override_failure_message("failed on after()").is_empty() + +func before_test() -> void: + assert_str("test before").override_failure_message("failed on before_test()").is_empty() + +func after_test() -> void: + assert_str("test after").is_equal("test after") + +func test_case1() -> void: + assert_str("test_case1").override_failure_message("failed 1 on test_case1()").is_empty() + assert_str("test_case1").override_failure_message("failed 2 on test_case1()").is_empty() + +func test_case2() -> void: + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterIntegrationTest.gd b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterIntegrationTest.gd new file mode 100644 index 0000000..ef17de1 --- /dev/null +++ b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterIntegrationTest.gd @@ -0,0 +1,26 @@ +class_name JUnitXmlReportWriterIntegrationTest +extends GdUnitReportWriterIntegrationTest + + +func before() -> void: + _setup_base("xml_reports/report_1", JUnitXmlReportWriter.new()) + + +func _patch_out_timings(content: String) -> String: + return _replace_timestamps(_replace_time_values(_replace_date_ids(content))) + + +func test_write_report() -> void: + var tests: Array[GdUnitTestCase] = [] + tests.append_array(_load_test_cases("TestSuiteAllStagesSuccess.resource")) + tests.append_array(_load_test_cases("TestSuiteFailOnMultipeStages.resource")) + tests.append_array(_load_test_cases("TestCaseSkipped.resource")) + _test_session = GdUnitTestSession.new(tests, _report_dir) + await run_tests(tests) + + _report_writer.write(_test_session.report_path, _report_summary) + + _assert_report_matches( + _report_dir + "/results.xml", + RESOURCE_REPORTS + "report_1/results.xml" + ) diff --git a/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterIntegrationTest.gd.uid b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterIntegrationTest.gd.uid new file mode 100644 index 0000000..25c68ba --- /dev/null +++ b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterIntegrationTest.gd.uid @@ -0,0 +1 @@ +uid://b7dggipwelipj diff --git a/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterTest.gd b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterTest.gd new file mode 100644 index 0000000..4772b46 --- /dev/null +++ b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterTest.gd @@ -0,0 +1,25 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name JUnitXmlReportWriterTest +extends GdUnitTestSuite + + +func test_to_time() -> void: + assert_str(JUnitXmlReportWriter.to_time(0)).is_equal("0.000") + assert_str(JUnitXmlReportWriter.to_time(1)).is_equal("0.001") + assert_str(JUnitXmlReportWriter.to_time(10)).is_equal("0.010") + assert_str(JUnitXmlReportWriter.to_time(100)).is_equal("0.100") + assert_str(JUnitXmlReportWriter.to_time(1000)).is_equal("1.000") + assert_str(JUnitXmlReportWriter.to_time(10123)).is_equal("10.123") + + +func test_to_type() -> void: + assert_str(JUnitXmlReportWriter.to_type(GdUnitReport.SUCCESS)).is_equal("SUCCESS") + assert_str(JUnitXmlReportWriter.to_type(GdUnitReport.WARN)).is_equal("WARN") + assert_str(JUnitXmlReportWriter.to_type(GdUnitReport.FAILURE)).is_equal("FAILURE") + assert_str(JUnitXmlReportWriter.to_type(GdUnitReport.ORPHAN)).is_equal("ORPHAN") + assert_str(JUnitXmlReportWriter.to_type(GdUnitReport.TERMINATED)).is_equal("TERMINATED") + assert_str(JUnitXmlReportWriter.to_type(GdUnitReport.INTERUPTED)).is_equal("INTERUPTED") + assert_str(JUnitXmlReportWriter.to_type(GdUnitReport.ABORT)).is_equal("ABORT") + assert_str(JUnitXmlReportWriter.to_type(1000)).is_equal("UNKNOWN") diff --git a/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterTest.gd.uid b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterTest.gd.uid new file mode 100644 index 0000000..9da782a --- /dev/null +++ b/addons/gdUnit4/test/reporters/xml/JUnitXmlReportWriterTest.gd.uid @@ -0,0 +1 @@ +uid://c0ea87weybj1f diff --git a/addons/gdUnit4/test/reporters/xml/XmlElementTest.gd b/addons/gdUnit4/test/reporters/xml/XmlElementTest.gd new file mode 100644 index 0000000..4f0a2f6 --- /dev/null +++ b/addons/gdUnit4/test/reporters/xml/XmlElementTest.gd @@ -0,0 +1,212 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name XmlElementTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/report/XmlElement.gd' + + +func test_attribute() -> void: + var element := XmlElement.new("testsuites")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "1")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "foo") + var expected :=""" + + + """.dedent().trim_prefix("\n").replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + +func test_empty() -> void: + var element := XmlElement.new("testsuites") + var expected := """ + + + """.dedent().trim_prefix("\n").replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_add_child() -> void: + var child := XmlElement.new("foo")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "1")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "foo") + var element := XmlElement.new("bar")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "1")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "bar")\ + .add_child(child) + var expected := """ + + + + + """.dedent().trim_prefix("\n").replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_add_childs() -> void: + var child_a := XmlElement.new("foo_a")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, 1)\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "foo_a") + var child_b := XmlElement.new("foo_b")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, 2)\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "foo_b") + var element := XmlElement.new("bar")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "1")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "bar")\ + .add_childs([child_a, child_b]) + var expected := """ + + + + + + + """.dedent().trim_prefix("\n").replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_add_text() -> void: + var element := XmlElement.new("testsuites")\ + .text("This is a message") + var expected := """ + + + + """.dedent().trim_prefix("\n").replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_complex_example() -> void: + var testsuite1 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "1")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "bar") + for test_case :int in [1,2,3,4,5]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "test_case_%d" % test_case) + testsuite1.add_child(test) + var testsuite2 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "2")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "bar2") + for test_case :int in [1,2,3]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "test_case_%d" % test_case) + if test_case == 2: + var failure := XmlElement.new("failure")\ + .attribute(JUnitXmlReportWriter.ATTR_MESSAGE, "test_case.gd:12")\ + .attribute(JUnitXmlReportWriter.ATTR_TYPE, "FAILURE")\ + .text("This is a failure\nExpecting true but was false\n") + test.add_child(failure) + testsuite2.add_child(test) + var root := XmlElement.new("testsuites")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "ID-XXX")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "report_foo")\ + .attribute(JUnitXmlReportWriter.ATTR_TESTS, 42)\ + .attribute(JUnitXmlReportWriter.ATTR_FAILURES, 1)\ + .attribute(JUnitXmlReportWriter.ATTR_TIME, "1.22")\ + .add_childs([testsuite1, testsuite2]) + var expected := """ + + + + + + + + + + + + + + + + + + + + + + + + + + """.dedent().trim_prefix("\n").replace("\r", "") + assert_str(root.to_xml()).is_equal(expected) + root.dispose() + + +func test_dispose() -> void: + var testsuite1 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "1")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "bar") + var testsuite1_expected_tests := Array() + for test_case :int in [1,2,3,4,5]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "test_case_%d" % test_case) + testsuite1.add_child(test) + testsuite1_expected_tests.append(test) + var testsuite2 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "2")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "bar2") + var testsuite2_expected_tests := Array() + for test_case :int in [1,2,3]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "test_case_%d" % test_case) + testsuite2_expected_tests.append(test) + if test_case == 2: + var failure := XmlElement.new("failure")\ + .attribute(JUnitXmlReportWriter.ATTR_MESSAGE, "test_case.gd:12")\ + .attribute(JUnitXmlReportWriter.ATTR_TYPE, "FAILURE")\ + .text("This is a failure\nExpecting true but was false\n") + test.add_child(failure) + testsuite2.add_child(test) + var root := XmlElement.new("testsuites")\ + .attribute(JUnitXmlReportWriter.ATTR_ID, "ID-XXX")\ + .attribute(JUnitXmlReportWriter.ATTR_NAME, "report_foo")\ + .attribute(JUnitXmlReportWriter.ATTR_TESTS, 42)\ + .attribute(JUnitXmlReportWriter.ATTR_FAILURES, 1)\ + .attribute(JUnitXmlReportWriter.ATTR_TIME, "1.22")\ + .add_childs([testsuite1, testsuite2]) + + assert_that(root._parent).is_null() + assert_array(root._childs).contains_exactly([testsuite1, testsuite2]) + assert_dict(root._attributes).has_size(5) + + assert_that(testsuite1._parent).is_equal(root) + assert_array(testsuite1._childs).contains_exactly(testsuite1_expected_tests) + assert_dict(testsuite1._attributes).has_size(2) + testsuite1_expected_tests.clear() + + assert_that(testsuite2._parent).is_equal(root) + assert_array(testsuite2._childs).contains_exactly(testsuite2_expected_tests) + assert_dict(testsuite2._attributes).has_size(2) + testsuite2_expected_tests.clear() + + # free all references + root.dispose() + assert_that(root._parent).is_null() + assert_array(root._childs).is_empty() + assert_dict(root._attributes).is_empty() + + assert_that(testsuite1._parent).is_null() + assert_array(testsuite1._childs).is_empty() + assert_dict(testsuite1._attributes).is_empty() + + assert_that(testsuite2._parent).is_null() + assert_array(testsuite2._childs).is_empty() + assert_dict(testsuite2._attributes).is_empty() diff --git a/addons/gdUnit4/test/reporters/xml/XmlElementTest.gd.uid b/addons/gdUnit4/test/reporters/xml/XmlElementTest.gd.uid new file mode 100644 index 0000000..b06d28e --- /dev/null +++ b/addons/gdUnit4/test/reporters/xml/XmlElementTest.gd.uid @@ -0,0 +1 @@ +uid://dk2xfuc5xfgsd diff --git a/addons/gdUnit4/test/resources/core/City.gd b/addons/gdUnit4/test/resources/core/City.gd new file mode 100644 index 0000000..a93e5d2 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/City.gd @@ -0,0 +1,8 @@ +class_name City +extends Node + +func name() -> String: + return "" + +func location() -> String: + return "" diff --git a/addons/gdUnit4/test/resources/core/City.gd.uid b/addons/gdUnit4/test/resources/core/City.gd.uid new file mode 100644 index 0000000..c68f270 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/City.gd.uid @@ -0,0 +1 @@ +uid://dgimkmhigmpoc diff --git a/addons/gdUnit4/test/resources/core/CustomClass.gd b/addons/gdUnit4/test/resources/core/CustomClass.gd new file mode 100644 index 0000000..3406ac6 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/CustomClass.gd @@ -0,0 +1,22 @@ +# example class with inner classes +class_name CustomClass +extends RefCounted + + +# an inner class +class InnerClassA extends Node: + var x: Variant + +# an inner class inherits form another inner class +class InnerClassB extends InnerClassA: + var y: Variant + +# an inner class +class InnerClassC: + + func foo() -> String: + return "foo" + +class InnerClassD: + class InnerInnerClassA: + var x: Variant diff --git a/addons/gdUnit4/test/resources/core/CustomClass.gd.uid b/addons/gdUnit4/test/resources/core/CustomClass.gd.uid new file mode 100644 index 0000000..d4c9c2d --- /dev/null +++ b/addons/gdUnit4/test/resources/core/CustomClass.gd.uid @@ -0,0 +1 @@ +uid://dugldod8wec20 diff --git a/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd b/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd new file mode 100644 index 0000000..c89dffe --- /dev/null +++ b/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd @@ -0,0 +1,8 @@ +class_name GeneratedPersonTest +extends GdUnitTestSuite + +# TestSuite generated from", +const __source = "res://addons/gdUnit4/test/resources/core/Person.gd" + +func test_name() -> void: + assert_that(Person.new().name()).is_equal("Hoschi") diff --git a/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd.uid b/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd.uid new file mode 100644 index 0000000..5fee69a --- /dev/null +++ b/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd.uid @@ -0,0 +1 @@ +uid://dpvdmhjs8k31b diff --git a/addons/gdUnit4/test/resources/core/Person.gd b/addons/gdUnit4/test/resources/core/Person.gd new file mode 100644 index 0000000..a05ea93 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/Person.gd @@ -0,0 +1,15 @@ +class_name Person +extends Resource + + +func name() -> String: + return "Hoschi" + +func last_name() -> String: + return "Horst" + +func age() -> int: + return 42 + +func street() -> String: + return "Route 66" diff --git a/addons/gdUnit4/test/resources/core/Person.gd.uid b/addons/gdUnit4/test/resources/core/Person.gd.uid new file mode 100644 index 0000000..34ad0ac --- /dev/null +++ b/addons/gdUnit4/test/resources/core/Person.gd.uid @@ -0,0 +1 @@ +uid://c0uolg845dou diff --git a/addons/gdUnit4/test/resources/core/Udo.gd b/addons/gdUnit4/test/resources/core/Udo.gd new file mode 100644 index 0000000..56f8fb7 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/Udo.gd @@ -0,0 +1,6 @@ +class_name Udo +extends Person + + +func _ready() -> void: + pass # Replace with function body. diff --git a/addons/gdUnit4/test/resources/core/Udo.gd.uid b/addons/gdUnit4/test/resources/core/Udo.gd.uid new file mode 100644 index 0000000..c94f677 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/Udo.gd.uid @@ -0,0 +1 @@ +uid://b6cguday1py37 diff --git a/addons/gdUnit4/test/resources/core/sources/TestPerson.cs b/addons/gdUnit4/test/resources/core/sources/TestPerson.cs new file mode 100644 index 0000000..a54ac36 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/sources/TestPerson.cs @@ -0,0 +1,28 @@ +using Godot; + +namespace Example.Test.Resources +{ + public partial class TestPerson + { + + public TestPerson(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + + public string FirstName { get; } + + public string LastName { get; } + + public string FullName => FirstName + " " + LastName; + + public string FullName2() => FirstName + " " + LastName; + + public string FullName3() + { + return FirstName + " " + LastName; + } + + } +} diff --git a/addons/gdUnit4/test/resources/excluded/.gdignore b/addons/gdUnit4/test/resources/excluded/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/resources/issues/gd-166/issue.gd b/addons/gdUnit4/test/resources/issues/gd-166/issue.gd new file mode 100644 index 0000000..f162a69 --- /dev/null +++ b/addons/gdUnit4/test/resources/issues/gd-166/issue.gd @@ -0,0 +1,21 @@ +extends Object + +const Type = preload("types.gd") + +var type := -1 : + get: + return type + set(value): + type = value + _set_type_name(value) + +var type_name :String + + +func _set_type(t:int) -> void: + type = t + + +func _set_type_name(type_ :int) -> void: + type_name = Type.to_str(type_) + print("type was set to %s" % type_name) diff --git a/addons/gdUnit4/test/resources/issues/gd-166/issue.gd.uid b/addons/gdUnit4/test/resources/issues/gd-166/issue.gd.uid new file mode 100644 index 0000000..61a49f6 --- /dev/null +++ b/addons/gdUnit4/test/resources/issues/gd-166/issue.gd.uid @@ -0,0 +1 @@ +uid://dirht85veg7ao diff --git a/addons/gdUnit4/test/resources/issues/gd-166/types.gd b/addons/gdUnit4/test/resources/issues/gd-166/types.gd new file mode 100644 index 0000000..63686fa --- /dev/null +++ b/addons/gdUnit4/test/resources/issues/gd-166/types.gd @@ -0,0 +1,10 @@ +extends Object + +enum { FOO, BAR, BAZ } + +static func to_str(type:int) -> String: + match type: + FOO: return "FOO" + BAR: return "BAR" + BAZ: return "BAZ" + return "" diff --git a/addons/gdUnit4/test/resources/issues/gd-166/types.gd.uid b/addons/gdUnit4/test/resources/issues/gd-166/types.gd.uid new file mode 100644 index 0000000..1aab381 --- /dev/null +++ b/addons/gdUnit4/test/resources/issues/gd-166/types.gd.uid @@ -0,0 +1 @@ +uid://xm8hb5bbl7fp diff --git a/addons/gdUnit4/test/spy/ClassWithEnumConstructor.gd b/addons/gdUnit4/test/spy/ClassWithEnumConstructor.gd new file mode 100644 index 0000000..ca31e8d --- /dev/null +++ b/addons/gdUnit4/test/spy/ClassWithEnumConstructor.gd @@ -0,0 +1,19 @@ +class_name ClassWithEnumConstructor +extends RefCounted + +enum MyEnumValue { + ONE = 10, + TWO = 20 +} + +var _value :MyEnumValue = MyEnumValue.ONE + + +# using an enum in the constructor +func _init(value :MyEnumValue, _second_parameter :PackedStringArray) -> void: + _value = value + + +# using an enum as function argument +func set_value(value :MyEnumValue) -> void: + _value = value diff --git a/addons/gdUnit4/test/spy/ClassWithEnumConstructor.gd.uid b/addons/gdUnit4/test/spy/ClassWithEnumConstructor.gd.uid new file mode 100644 index 0000000..8252d32 --- /dev/null +++ b/addons/gdUnit4/test/spy/ClassWithEnumConstructor.gd.uid @@ -0,0 +1 @@ +uid://dimhp5bymucdm diff --git a/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd b/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd new file mode 100644 index 0000000..43595ec --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd @@ -0,0 +1,46 @@ +# GdUnit generated TestSuite +@warning_ignore_start("unsafe_method_access") +class_name GdUnitSpyBuilderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd' + + +class NodeWithOutVirtualFunc extends Node: + func _ready() -> void: + pass + + #func _input(event :InputEvent) -> void: + + +func test_spy_on_script_respect_virtual_functions() -> void: + var do_spy :Variant = auto_free(GdUnitSpyBuilder.spy_on_script(auto_free(NodeWithOutVirtualFunc.new()), [], true).new()) + + do_spy.__init_doubler() + do_spy.__init([]) + assert_bool(do_spy.has_method("_ready")).is_true() + assert_bool(do_spy.has_method("_input")).is_false() + + +func test_spy_on_scene_with_onready_parameters() -> void: + # setup a scene with holding parameters + var scene: TestSceneWithProperties = load("res://addons/gdUnit4/test/spy/resources/TestSceneWithProperties.tscn").instantiate() + add_child(scene) + + # precheck the parameters are original set + var original_progress_value: Variant = scene.progress + assert_object(original_progress_value).is_not_null() + assert_object(scene._parameter_obj).is_not_null() + assert_dict(scene._parameter_dict).is_equal({"key" : "value"}) + + # create spy on scene + var spy_scene :Variant = spy(scene) + + # verify the @onready property is set + assert_object(spy_scene.progress).is_same(original_progress_value) + # verify all properties are set + assert_object(spy_scene._parameter_obj).is_not_null().is_same(scene._parameter_obj) + assert_dict(spy_scene._parameter_dict).is_not_null().is_same(scene._parameter_dict) + + scene.free() diff --git a/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd.uid b/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd.uid new file mode 100644 index 0000000..452ef05 --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd.uid @@ -0,0 +1 @@ +uid://c5vbju4pivoes diff --git a/addons/gdUnit4/test/spy/GdUnitSpyCallableTest.gd b/addons/gdUnit4/test/spy/GdUnitSpyCallableTest.gd new file mode 100644 index 0000000..c2c0cd6 --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyCallableTest.gd @@ -0,0 +1,286 @@ +@warning_ignore_start("unsafe_method_access") +extends GdUnitTestSuite + +var _rpc_called: String = "" + + +@rpc("call_local") +func do_rpc(rpc_name: String, value: Variant) -> void: + _rpc_called = rpc_name + str(value) + + +@rpc("call_local", "any_peer", "unreliable", 1) +func do_rpc_on_peer(arg1: Variant, arg2: Variant) -> void: + _rpc_called = str(arg1) + str(arg2) + + +func test_callable_functions() -> void: + # supported Callable functions to double see https://docs.godotengine.org/en/stable/classes/class_callable.html#methods + assert_array(CallableDoubler.callable_functions()).contains_exactly_in_any_order([ + # we allow to default constructor, is need to create at least the spy + "_init", + "bind", + "bindv", + "call", + "call_deferred", + "callv", + "get_bound_arguments", + "get_bound_arguments_count", + "get_method", + "get_object", + "get_object_id", + "hash", + "is_custom", + "is_null", + "is_standard", + "is_valid", + "rpc", + "rpc_id", + "unbind"]) + + +func test_exclude_functions() -> void: + assert_array(CallableDoubler.excluded_functions())\ + # it should not exclude callable function + .not_contains(CallableDoubler.callable_functions()) + + + +func test_call() -> void: + var cb := func(x: int) -> String: + return "is_called %s" % x + var cb_spy :Variant = spy(cb) + + # do use the spy to call the callable + var result :Variant = cb_spy.call(42) + # verify is called by validate the result + assert_that(result).is_equal("is_called 42") + # verify should be successfull + verify(cb_spy).call(42) + # verify with a not used argument must fail + assert_failure(func() -> void: verify(cb_spy).call(23)).is_failed() + + +func test_call_with_binded_value() -> void: + var cb := func(x: int, y: int) -> String: + return "is_called %s.%s" % [x, y] + var cb_spy :Variant = spy(cb.bind(24)) + + # do use the spy to call the callable + var result :Variant = cb_spy.call(42) + # verify is called by validate the result + assert_that(result).is_equal("is_called 42.24") + # verify should be successfull + verify(cb_spy).call(42) + # verify with a not used argument must fail + assert_failure(func() -> void: verify(cb_spy).call(23)).is_failed() + + +# we not able to stub on callv because the original signature of Callabe:callv and Object:callv are different +# and a spy is a specialized object delegator to the original implementation as object instance +# a Callable is not inherits form object so it makes it impossible to spy/mock on `callv` +func _test_callv() -> void: + var cb := func(x: int, y: int) -> String: + return "is_called %s.%s" % [x, y] + var cb_spy :Variant = spy(cb.bind(24)) + + # do use the spy to call the callable + var result :Variant = cb_spy.callv([42]) + # verify is called by validate the result + assert_that(result).is_equal("is_called 42.24") + # verify should be successfull + verify(cb_spy).callv([42, 24]) + + +func test_bind_vararg() -> void: + var cb := func(x: int, y: int) -> String: + return "is_called %s.%s" % [x, y] + var cb_spy :Variant = spy(cb) + cb_spy.bind(24, 25, 26) + assert_array(cb_spy.get_bound_arguments()).contains_exactly([24, 25, 26]) + + +func test_bind() -> void: + var cb := func(x: int, y: int) -> String: + return "is_called %s.%s" % [x, y] + var cb_spy :Variant = spy(cb) + cb_spy.bind(24) + assert_array(cb_spy.get_bound_arguments()).contains_exactly([24]) + + # verify bind is called + verify(cb_spy).bind(24) + # verify bind is not called with 23 + assert_failure(func() -> void: verify(cb_spy).bind(23)).is_failed() + + # do use the spy to call the callable + var result :Variant = cb_spy.call(42) + # verify is called by validate the result + assert_that(result).is_equal("is_called 42.24") + # verify should be successfull + verify(cb_spy).call(42) + + +func test_bindv() -> void: + var cb := func(a1: int, a2: int, a3: int, a4: int) -> String: + return "is_called %s %s %s %s" % [a1, a2, a3, a4] + var cb_spy :Variant = spy(cb) + cb_spy.bindv([21,22,23]) + + # verify bindv is called + verify(cb_spy).bindv([21,22,23]) + # verify bindv is not called with [21,22,27] + assert_failure(func() -> void: verify(cb_spy).bindv([21,22,27])).is_failed() + # finally check via call it resolves all values + assert_str(cb_spy.call(42)).is_equal("is_called 42 21 22 23") + + +func test_unbind() -> void: + var cb := func(a1: int, a2: int, a3: int) -> String: + return "is_called %s %s %s" % [a1, a2, a3] + var cb_spy :Variant = spy(cb.bindv([21,22,23])) + cb_spy.unbind(1) + + # verify unbind is called + verify(cb_spy).unbind(1) + # verify unbind is not called with argument 2 + assert_failure(func() -> void: verify(cb_spy).unbind(2)).is_failed() + # finally check via call it resolves all values + assert_str(cb_spy.call(42)).is_equal("is_called 21 22 23") + + +func test_rpc() -> void: + _rpc_called = "" + var cb_spy :Variant = spy(do_rpc) + cb_spy.rpc("abc", 23) + # verify the callable @rpc is called + assert_that(_rpc_called).is_equal("abc23") + + # verify rpc is called + verify(cb_spy).rpc("abc", 23) + # verify rpc is not called with this argument s + assert_failure(func() -> void: verify(cb_spy).rpc("abc", 43)).is_failed() + + +func test_rpc_id() -> void: + _rpc_called = "" + var cb_spy :Variant = spy(do_rpc_on_peer) + cb_spy.rpc_id(1, "foo", "23") + # verify the callable @rpc is called + assert_that(_rpc_called).is_equal("foo23") + + # verify rpc_id is called + verify(cb_spy).rpc_id(1, "foo", "23") + # verify rpc_id is not called with this argument s + assert_failure(func() -> void: verify(cb_spy).rpc_id(23, "arg1", "arg2")).is_failed() + + +func test_get_bound_arguments() -> void: + var cb := func() -> void: return + var cb_syp :Variant = spy(cb) + + verify(cb_syp, 0).get_bound_arguments() + cb_syp.bind(1,2,3) + assert_array(cb_syp.get_bound_arguments()).contains_exactly([1,2,3]) + verify(cb_syp, 1).get_bound_arguments() + + +func test_get_bound_arguments_count() -> void: + var cb := func() -> void: return + var cb_syp :Variant = spy(cb) + + verify(cb_syp, 0).get_bound_arguments_count() + cb_syp.bind(1,2,3) + assert_that(cb_syp.get_bound_arguments_count()).is_equal(3) + verify(cb_syp, 1).get_bound_arguments_count() + + +func test_get_method() -> void: + var cb_syp :Variant = spy(do_rpc) + + verify(cb_syp, 0).get_method() + assert_that(cb_syp.get_method()).is_equal("do_rpc") + verify(cb_syp, 1).get_method() + + +func test_get_object() -> void: + var cb_syp :Variant = spy(do_rpc) + + verify(cb_syp, 0).get_object() + assert_that(cb_syp.get_object()).is_same(self) + verify(cb_syp, 1).get_object() + + +func test_get_object_id() -> void: + var cb_syp :Variant = spy(do_rpc) + + verify(cb_syp, 0).get_object_id() + assert_that(cb_syp.get_object_id()).is_equal(get_instance_id()) + verify(cb_syp, 1).get_object_id() + + +func test_hash() -> void: + var cb_syp :Variant = spy(do_rpc) + + verify(cb_syp, 0).hash() + assert_that(cb_syp.hash()).is_equal(do_rpc.hash()) + verify(cb_syp, 1).hash() + + +func test_is_custom_is_true() -> void: + var cb_syp :Variant = spy(do_rpc) + assert_that(do_rpc.is_custom()).is_true() + + verify(cb_syp, 0).is_custom() + assert_that(cb_syp.is_custom()).is_true() + verify(cb_syp, 1).is_custom() + + +func test_is_custom_is_false() -> void: + var cb := Callable(self, "do_rpc") + assert_that(cb.is_custom()).is_false() + + var cb_syp :Variant = spy(cb) + verify(cb_syp, 0).is_custom() + assert_that(cb_syp.is_custom()).is_false() + verify(cb_syp, 1).is_custom() + + +func test_is_null() -> void: + var cb_syp :Variant = spy(do_rpc) + + verify(cb_syp, 0).is_null() + assert_that(cb_syp.is_null()).is_false() + verify(cb_syp, 1).is_null() + + cb_syp = spy(Callable()) + assert_that(cb_syp.is_null()).is_true() + + +func test_is_standard_is_false() -> void: + var cb_syp :Variant = spy(do_rpc) + assert_that(do_rpc.is_standard()).is_false() + + verify(cb_syp, 0).is_standard() + assert_that(cb_syp.is_standard()).is_false() + verify(cb_syp, 1).is_standard() + + +func test_is_standard_is_true() -> void: + var cb := Callable(self, "do_rpc") + assert_that(cb.is_standard()).is_true() + + var cb_syp :Variant = spy(cb) + verify(cb_syp, 0).is_standard() + assert_that(cb_syp.is_standard()).is_true() + verify(cb_syp, 1).is_standard() + + +func test_is_valid() -> void: + var cb_syp :Variant = spy(do_rpc) + + verify(cb_syp, 0).is_valid() + assert_that(cb_syp.is_valid()).is_true() + verify(cb_syp, 1).is_valid() + + cb_syp = spy(Callable()) + assert_that(cb_syp.is_valid()).is_false() diff --git a/addons/gdUnit4/test/spy/GdUnitSpyCallableTest.gd.uid b/addons/gdUnit4/test/spy/GdUnitSpyCallableTest.gd.uid new file mode 100644 index 0000000..ac2dfcb --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyCallableTest.gd.uid @@ -0,0 +1 @@ +uid://dvh61lpcxonhg diff --git a/addons/gdUnit4/test/spy/GdUnitSpyTest.gd b/addons/gdUnit4/test/spy/GdUnitSpyTest.gd new file mode 100644 index 0000000..15c7ffa --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyTest.gd @@ -0,0 +1,755 @@ +class_name GdUnitSpyTest +extends GdUnitTestSuite + + +func before() -> void: + # we disable the error reporting to do not spam the logs + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_ERRORS, false) + + +func test_spy_instance_id_is_unique() -> void: + var m1 :Variant = spy(RefCounted.new()) + var m2 :Variant = spy(RefCounted.new()) + # test the internal instance id is unique + assert_object(m1).is_not_same(m2) + + +func test_cant_spy_is_not_a_instance() -> void: + # returns null because spy needs an 'real' instance to by spy checked + var spy_node :Variant = spy(Node) + assert_object(spy_node).is_null() + + +func test_spy_on_Node() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + # verify we have no interactions currently checked this instance + verify_no_interactions(spy_node) + + assert_object(spy_node)\ + .is_not_null()\ + .is_instanceof(Node)\ + .is_not_same(instance) + + # call first time + @warning_ignore("unsafe_method_access") + spy_node.set_process(false) + + # verify is called one times@warning_ignore("unsafe_method_access") + @warning_ignore("unsafe_method_access") + verify(spy_node).set_process(false) + # just double check that verify has no affect to the counter + @warning_ignore("unsafe_method_access") + verify(spy_node).set_process(false) + + # call a scond time + @warning_ignore("unsafe_method_access") + spy_node.set_process(false) + # verify is called two times + @warning_ignore("unsafe_method_access") + verify(spy_node, 2).set_process(false) + @warning_ignore("unsafe_method_access") + verify(spy_node, 2).set_process(false) + + +func test_spy_source_with_class_name_by_resource_path() -> void: + var instance :Object = auto_free(_load('res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd').new()) + var m :Variant = spy(instance) + @warning_ignore("unsafe_method_access") + var head :String = m.get_script().source_code.substr(0, 500) + assert_str(head)\ + .contains("class_name DoubledSpyClassMunderwoodPathingWorld")\ + .contains("extends 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd'") + + +func test_spy_source_with_class_name_by_class() -> void: + var m :Variant = spy(auto_free(Munderwood_Pathing_World.new())) + @warning_ignore("unsafe_method_access") + var head :String = m.get_script().source_code.substr(0, 500) + assert_str(head)\ + .contains("class_name DoubledSpyClassMunderwoodPathingWorld")\ + .contains("extends 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd'") + + +func test_spy_extends_godot_class() -> void: + var m :Variant = spy(auto_free(World3D.new())) + @warning_ignore("unsafe_method_access") + var head :String = m.get_script().source_code.substr(0, 500) + assert_str(head)\ + .contains("class_name DoubledSpyClassWorld")\ + .contains("extends World3D") + + +func test_spy_on_custom_class() -> void: + var instance :AdvancedTestClass = auto_free(AdvancedTestClass.new()) + var spy_instance :Variant = spy(instance) + + # verify we have currently no interactions + verify_no_interactions(spy_instance) + + assert_object(spy_instance)\ + .is_not_null()\ + .is_instanceof(AdvancedTestClass)\ + .is_not_same(instance) + + @warning_ignore("unsafe_method_access") + spy_instance.setup_local_to_scene() + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).setup_local_to_scene() + + # call first time script func with different arguments + @warning_ignore("unsafe_method_access") + spy_instance.get_area("test_a") + @warning_ignore("unsafe_method_access") + spy_instance.get_area("test_b") + @warning_ignore("unsafe_method_access") + spy_instance.get_area("test_c") + + # verify is each called only one time for different arguments + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).get_area("test_a") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).get_area("test_b") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).get_area("test_c") + # an second call with arg "test_c" + @warning_ignore("unsafe_method_access") + spy_instance.get_area("test_c") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).get_area("test_a") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).get_area("test_b") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 2).get_area("test_c") + + # verify if a not used argument not counted + @warning_ignore("unsafe_method_access") + verify(spy_instance, 0).get_area("test_no") + + +# GD-291 https://github.com/MikeSchulze/gdUnit4/issues/291 +func test_spy_class_with_custom_formattings() -> void: + var resource := _load("res://addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd") + var do_spy :Variant = spy(auto_free(resource.new("test"))) + @warning_ignore("unsafe_method_access") + do_spy.a1("set_name", "", true) + @warning_ignore("unsafe_method_access") + verify(do_spy, 1).a1("set_name", "", true) + verify_no_more_interactions(do_spy) + assert_failure(func() -> void: verify_no_interactions(do_spy))\ + .is_failed() \ + .has_line(140) + + +func test_spy_copied_class_members() -> void: + var instance: TestPerson = auto_free(_load("res://addons/gdUnit4/test/mocker/resources/TestPerson.gd").new("user-x", "street", 56616)) + assert_that(instance._name).is_equal("user-x") + assert_that(instance._value).is_equal(1024) + assert_that(instance._address._street).is_equal("street") + assert_that(instance._address._code).is_equal(56616) + + # spy it + var spy_instance :Variant = spy(instance) + reset(spy_instance) + + # verify members are inital copied + assert_that(spy_instance._name).is_equal("user-x") + assert_that(spy_instance._value).is_equal(1024) + assert_that(spy_instance._address._street).is_equal("street") + assert_that(spy_instance._address._code).is_equal(56616) + + spy_instance._value = 2048 + assert_that(instance._value).is_equal(1024) + assert_that(spy_instance._value).is_equal(2048) + + +func test_spy_copied_class_members_on_node() -> void: + var node :Node = auto_free(Node.new()) + # checked a fresh node the name is empty and results into a error when copied at spy + # E 0:00:01.518 set_name: Condition "name == """ is true. + # C++ Source> scene/main/node.cpp:934 @ set_name() + # we set a placeholder instead + assert_that(spy(node).name).is_equal("") + + node.set_name("foo") + assert_that(spy(node).name).is_equal("foo") + + +func test_spy_on_inner_class() -> void: + var instance :AdvancedTestClass.AtmosphereData = auto_free(AdvancedTestClass.AtmosphereData.new()) + var spy_instance :AdvancedTestClass.AtmosphereData = spy(instance) + + # verify we have currently no interactions + verify_no_interactions(spy_instance) + + assert_object(spy_instance)\ + .is_not_null()\ + .is_instanceof(AdvancedTestClass.AtmosphereData)\ + .is_not_same(instance) + + spy_instance.set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.2) + spy_instance.set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.3) + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.2) + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.3) + + +func test_spy_on_singleton() -> void: + assert_error(func () -> void: + var spy_node_ :Variant = spy(Input) + assert_object(spy_node_).is_null() + await await_idle_frame()).is_push_error("Spy on a Singleton is not allowed! 'Input'") + + +func test_example_verify() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + # verify we have no interactions currently checked this instance + verify_no_interactions(spy_node) + + # call with different arguments + @warning_ignore("unsafe_method_access") + spy_node.set_process(false) # 1 times + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 1 times + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 2 times + + # verify how often we called the function with different argument + @warning_ignore("unsafe_method_access") + verify(spy_node, 2).set_process(true) # in sum two times with true + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).set_process(false)# in sum one time with false + + # verify total sum by using an argument matcher + @warning_ignore("unsafe_method_access") + verify(spy_node, 3).set_process(any_bool()) + + +func test_verify_fail() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + # interact two time + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 1 times + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 2 times + + # verify we interacts two times + @warning_ignore("unsafe_method_access") + verify(spy_node, 2).set_process(true) + + # verify should fail because we interacts two times and not one + var expected_error := """ + Expecting interaction on: + 'set_process(true :bool)' 1 time's + But found interactions on: + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n") + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: verify(spy_node, 1).set_process(true)) \ + .is_failed() \ + .has_line(254) \ + .has_message(expected_error) + + +func test_verify_func_interaction_wiht_PoolStringArray() -> void: + var spy_instance :ClassWithPoolStringArrayFunc = spy(ClassWithPoolStringArrayFunc.new()) + + spy_instance.set_values(PackedStringArray()) + + @warning_ignore("unsafe_method_access") + verify(spy_instance).set_values(PackedStringArray()) + verify_no_more_interactions(spy_instance) + + +func test_verify_func_interaction_wiht_PackedStringArray_fail() -> void: + var spy_instance :ClassWithPoolStringArrayFunc = spy(ClassWithPoolStringArrayFunc.new()) + + spy_instance.set_values(PackedStringArray()) + + # try to verify with default array type instead of PackedStringArray type + var expected_error := """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's""" \ + .dedent().trim_prefix("\n") + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: verify(spy_instance, 1).set_values([])) \ + .is_failed() \ + .has_line(283) \ + .has_message(expected_error) + + reset(spy_instance) + # try again with called two times and different args + spy_instance.set_values(PackedStringArray()) + spy_instance.set_values(PackedStringArray(["a", "b"])) + spy_instance.set_values([1, 2]) + expected_error = """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's + 'set_values(["a", "b"] :PackedStringArray)' 1 time's + 'set_values([1, 2] :Array)' 1 time's""" \ + .dedent().trim_prefix("\n") + @warning_ignore("unsafe_method_access") + assert_failure(func() -> void: verify(spy_instance, 1).set_values([])) \ + .is_failed() \ + .has_line(302) \ + .has_message(expected_error) + + +func test_reset() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + # call with different arguments + @warning_ignore("unsafe_method_access") + spy_node.set_process(false) # 1 times + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 1 times + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 2 times + + @warning_ignore("unsafe_method_access") + verify(spy_node, 2).set_process(true) + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).set_process(false) + + # now reset the spy + reset(spy_node) + # verify all counters have been reset + verify_no_interactions(spy_node) + + +func test_verify_no_interactions() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + # verify we have no interactions checked this mock + verify_no_interactions(spy_node) + + +func test_verify_no_interactions_fails() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + # interact + @warning_ignore("unsafe_method_access") + spy_node.set_process(false) # 1 times + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 1 times + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) # 2 times + + var expected_error :=""" + Expecting no more interactions! + But found interactions on: + 'set_process(false :bool)' 1 time's + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n") + # it should fail because we have interactions + assert_failure(func() -> void: verify_no_interactions(spy_node)) \ + .is_failed() \ + .has_line(358) \ + .has_message(expected_error) + + +func test_verify_no_more_interactions() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + @warning_ignore("unsafe_method_access") + spy_node.is_ancestor_of(instance) + @warning_ignore("unsafe_method_access") + spy_node.set_process(false) + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) + + # verify for called functions + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).is_ancestor_of(instance) + @warning_ignore("unsafe_method_access") + verify(spy_node, 2).set_process(true) + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).set_process(false) + + # There should be no more interactions checked this mock + verify_no_more_interactions(spy_node) + + +func test_verify_no_more_interactions_but_has() -> void: + var instance :Node = auto_free(Node.new()) + var spy_node :Variant = spy(instance) + + @warning_ignore("unsafe_method_access") + spy_node.is_ancestor_of(instance) + @warning_ignore("unsafe_method_access") + spy_node.set_process(false) + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) + @warning_ignore("unsafe_method_access") + spy_node.set_process(true) + + # now we simulate extra calls that we are not explicit verify + @warning_ignore("unsafe_method_access") + spy_node.is_inside_tree() + @warning_ignore("unsafe_method_access") + spy_node.is_inside_tree() + # a function with default agrs + @warning_ignore("unsafe_method_access") + spy_node.find_child("mask") + # same function again with custom agrs + @warning_ignore("unsafe_method_access") + spy_node.find_child("mask", false, false) + + # verify 'all' exclusive the 'extra calls' functions + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).is_ancestor_of(instance) + @warning_ignore("unsafe_method_access") + verify(spy_node, 2).set_process(true) + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).set_process(false) + + # now use 'verify_no_more_interactions' to check we have no more interactions checked this mock + # but should fail with a collecion of all not validated interactions + var expected_error :=""" + Expecting no more interactions! + But found interactions on: + 'is_inside_tree()' 2 time's + 'find_child(mask :String, true :bool, true :bool)' 1 time's + 'find_child(mask :String, false :bool, false :bool)' 1 time's""" \ + .dedent().trim_prefix("\n") + assert_failure(func() -> void: verify_no_more_interactions(spy_node)) \ + .is_failed() \ + .has_line(431) \ + .has_message(expected_error) + + +class ClassWithStaticFunctions: + + static func foo() -> void: + pass + + static func bar() -> void: + pass + + +func test_create_spy_static_func_untyped() -> void: + var instance :Variant = spy(ClassWithStaticFunctions.new()) + assert_object(instance).is_not_null() + + +func test_spy_snake_case_named_class_by_resource_path() -> void: + var instance_a :Object = _load("res://addons/gdUnit4/test/mocker/resources/snake_case.gd").new() + var spy_a :Variant = spy(instance_a) + assert_object(spy_a).is_not_null() + + @warning_ignore("unsafe_method_access") + spy_a.custom_func() + @warning_ignore("unsafe_method_access") + verify(spy_a).custom_func() + verify_no_more_interactions(spy_a) + + var instance_b :Object = _load("res://addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd").new() + var spy_b :Variant = spy(instance_b) + assert_object(spy_b).is_not_null() + + @warning_ignore("unsafe_method_access") + spy_b.custom_func() + @warning_ignore("unsafe_method_access") + verify(spy_b).custom_func() + verify_no_more_interactions(spy_b) + + +func test_spy_snake_case_named_class_by_class() -> void: + var do_spy :Variant = spy(snake_case_class_name.new()) + assert_object(do_spy).is_not_null() + + @warning_ignore("unsafe_method_access") + do_spy.custom_func() + @warning_ignore("unsafe_method_access") + verify(do_spy).custom_func() + verify_no_more_interactions(do_spy) + + # try checked Godot class + var spy_tcp_server :Variant = spy(TCPServer.new()) + assert_object(spy_tcp_server).is_not_null() + + @warning_ignore("unsafe_method_access") + spy_tcp_server.is_listening() + @warning_ignore("unsafe_method_access") + spy_tcp_server.is_connection_available() + @warning_ignore("unsafe_method_access") + verify(spy_tcp_server).is_listening() + @warning_ignore("unsafe_method_access") + verify(spy_tcp_server).is_connection_available() + verify_no_more_interactions(spy_tcp_server) + + +const Issue = preload("res://addons/gdUnit4/test/resources/issues/gd-166/issue.gd") +const Type = preload("res://addons/gdUnit4/test/resources/issues/gd-166/types.gd") + + +func test_spy_preload_class_GD_166() -> void: + var instance :Object = auto_free(Issue.new()) + var spy_instance :Variant = spy(instance) + + spy_instance.type = Type.FOO + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1)._set_type_name(Type.FOO) + assert_int(spy_instance.type).is_equal(Type.FOO) + assert_str(spy_instance.type_name).is_equal("FOO") + + +# https://github.com/MikeSchulze/gdUnit4/issues/38 +func test_spy_Node_use_real_func_vararg() -> void: + # setup + var instance :Variant = auto_free(Node.new()) + + @warning_ignore("unsafe_method_access") + var spy_node :Variant = spy(instance) + assert_that(spy_node).is_not_null() + + # test emit it + @warning_ignore("unsafe_method_access") + spy_node.emit_signal("ready", "aa", "bb", "cc") + + # verify is emitted + @warning_ignore("unsafe_method_access") + verify(spy_node).emit_signal("ready", "aa", "bb", "cc") + + # test emit it + @warning_ignore("unsafe_method_access") + spy_node.emit_signal("ready", "aa", "xxx") + # verify is emitted + @warning_ignore("unsafe_method_access") + verify(spy_node).emit_signal("ready", "aa", "xxx") + + +class ClassWithSignal: + signal test_signal_a + signal test_signal_b + + func foo(arg :int) -> void: + if arg == 0: + emit_signal(test_signal_a.get_name(), "aa") + else: + emit_signal(test_signal_b.get_name(), "bb", true) + + func bar(arg :int) -> bool: + if arg == 0: + emit_signal(test_signal_a.get_name(), "aa") + else: + emit_signal(test_signal_b.get_name(), "bb", true) + return true + + @warning_ignore("unused_parameter") + func do_call(name: String, ...varargs: Array) -> void: + pass + + +func test_spy_verify_variadic_args() -> void: + var spy_instance :Variant = spy(ClassWithSignal.new()) + + @warning_ignore("unsafe_method_access") + spy_instance.do_call("foo", 1, 2, 3) + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).do_call("foo", 1, 2, 3) + + +# https://github.com/MikeSchulze/gdUnit4/issues/14 +func _test_spy_verify_emit_signal() -> void: + var spy_instance :Variant = spy(ClassWithSignal.new()) + assert_that(spy_instance).is_not_null() + + @warning_ignore("unsafe_method_access") + spy_instance.foo(0) + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).emit_signal(spy_instance.test_signal_a.get_name(), "aa") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 0).emit_signal(spy_instance.test_signal_b.get_name(), "bb", true) + reset(spy_instance) + + @warning_ignore("unsafe_method_access") + spy_instance.foo(1) + @warning_ignore("unsafe_method_access") + verify(spy_instance, 0).emit_signal(spy_instance.test_signal_a.get_name(), "aa") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).emit_signal(spy_instance.test_signal_b.get_name(), "bb", true) + reset(spy_instance) + + @warning_ignore("unsafe_method_access") + spy_instance.bar(0) + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).emit_signal(spy_instance.test_signal_a.get_name(), "aa") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 0).emit_signal(spy_instance.test_signal_b.get_name(), "bb", true) + reset(spy_instance) + + @warning_ignore("unsafe_method_access") + spy_instance.bar(1) + @warning_ignore("unsafe_method_access") + verify(spy_instance, 0).emit_signal(spy_instance.test_signal_a.get_name(), "aa") + @warning_ignore("unsafe_method_access") + verify(spy_instance, 1).emit_signal(spy_instance.test_signal_b.get_name(), "bb", true) + + +func test_spy_func_with_default_build_in_type() -> void: + var spy_instance :ClassWithDefaultBuildIntTypes = spy(ClassWithDefaultBuildIntTypes.new()) + assert_object(spy_instance).is_not_null() + # call with default arg + spy_instance.foo("abc") + spy_instance.bar("def") + + @warning_ignore("unsafe_method_access") + verify(spy_instance).foo("abc", Color.RED) + @warning_ignore("unsafe_method_access") + verify(spy_instance).bar("def", Vector3.FORWARD, AABB()) + verify_no_more_interactions(spy_instance) + + # call with custom args + spy_instance.foo("abc", Color.BLUE) + spy_instance.bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + @warning_ignore("unsafe_method_access") + verify(spy_instance).foo("abc", Color.BLUE) + @warning_ignore("unsafe_method_access") + verify(spy_instance).bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + verify_no_more_interactions(spy_instance) + + +func test_spy_scene_by_resource_path() -> void: + var spy_scene :Variant = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(spy_scene)\ + .is_not_null()\ + .is_not_instanceof(PackedScene)\ + .is_instanceof(Control) + @warning_ignore("unsafe_method_access") + assert_str(spy_scene.get_script().resource_name).is_equal("SpyTestScene.gd") + # check is spyed scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(spy_scene)).is_true() + + +func test_spy_on_PackedScene() -> void: + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var original_script :Script = resource.get_script() + assert_object(resource).is_instanceof(PackedScene) + + var spy_scene :Variant = spy(resource) + + assert_object(spy_scene)\ + .is_not_null()\ + .is_not_instanceof(PackedScene)\ + .is_not_same(resource) + @warning_ignore("unsafe_method_access") + assert_object(spy_scene.get_script())\ + .is_not_null()\ + .is_instanceof(GDScript)\ + .is_not_same(original_script) + @warning_ignore("unsafe_method_access") + assert_str(spy_scene.get_script().resource_name).is_equal("SpyTestScene.gd") + # check is spyed scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(spy_scene)).is_true() + + +func test_spy_scene_by_instance() -> void: + var resource: PackedScene = load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var instance :Control = resource.instantiate() + var original_script :Script = instance.get_script() + var spy_scene :Variant = spy(instance) + + assert_object(spy_scene)\ + .is_not_null()\ + .is_same(instance)\ + .is_instanceof(Control) + @warning_ignore("unsafe_method_access") + assert_object(spy_scene.get_script())\ + .is_not_null()\ + .is_instanceof(GDScript)\ + .is_not_same(original_script) + @warning_ignore("unsafe_method_access") + assert_str(spy_scene.get_script().resource_name).is_equal("SpyTestScene.gd") + # check is mocked scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(spy_scene)).is_true() + + +func test_spy_scene_by_path_fail_has_no_script_attached() -> void: + var resource: PackedScene = load("res://addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn") + var instance :Control = auto_free(resource.instantiate()) + + # has to fail and return null + var spy_scene :Variant = spy(instance) + assert_object(spy_scene).is_null() + + +func test_spy_scene_initalize() -> void: + var spy_scene :Variant = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(spy_scene).is_not_null() + + # Add as child to a scene tree to trigger _ready to initalize all variables + @warning_ignore("unsafe_cast") + add_child(spy_scene as Node) + # ensure _ready is recoreded and onyl once called + @warning_ignore("unsafe_method_access") + verify(spy_scene, 1)._ready() + @warning_ignore("unsafe_method_access") + verify(spy_scene, 1).only_one_time_call() + assert_object(spy_scene._box1).is_not_null() + assert_object(spy_scene._box2).is_not_null() + assert_object(spy_scene._box3).is_not_null() + + # check signals are connected + @warning_ignore("unsafe_cast", "unsafe_method_access") + assert_bool(spy_scene.is_connected("panel_color_change", Callable(spy_scene as Node, "_on_panel_color_changed"))) + + # check exports + @warning_ignore("unsafe_method_access") + assert_str(spy_scene._initial_color.to_html()).is_equal(Color.RED.to_html()) + + +class CustomNode extends Node: + + func _ready() -> void: + # we call this function to verify the _ready is only once called + # this is need to verify `add_child` is calling the original implementation only once + only_one_time_call() + + func only_one_time_call() -> void: + pass + + +func test_spy_ready_called_once() -> void: + var spy_node :Variant = spy(auto_free(CustomNode.new())) + + # Add as child to a scene tree to trigger _ready to initalize all variables + @warning_ignore("unsafe_cast") + add_child(spy_node as Node) + + # ensure _ready is recoreded and onyl once called + @warning_ignore("unsafe_method_access") + verify(spy_node, 1)._ready() + @warning_ignore("unsafe_method_access") + verify(spy_node, 1).only_one_time_call() + + +func test_spy_with_enum_in_constructor() -> void: + # this test uses a class with an enum in the constructor + var unit := ClassWithEnumConstructor.new(ClassWithEnumConstructor.MyEnumValue.TWO, []) + var s :Variant = spy(unit) + @warning_ignore("unsafe_method_access") + s.set_value(ClassWithEnumConstructor.MyEnumValue.ONE) + # test + @warning_ignore("unsafe_method_access") + verify(s, 1).set_value(ClassWithEnumConstructor.MyEnumValue.ONE) + + +func _load(resource_path: String) -> GDScript: + return load(resource_path) diff --git a/addons/gdUnit4/test/spy/GdUnitSpyTest.gd.uid b/addons/gdUnit4/test/spy/GdUnitSpyTest.gd.uid new file mode 100644 index 0000000..b74d94a --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyTest.gd.uid @@ -0,0 +1 @@ +uid://cp6ost5re5fwe diff --git a/addons/gdUnit4/test/spy/resources/TestSceneWithProperties.tscn b/addons/gdUnit4/test/spy/resources/TestSceneWithProperties.tscn new file mode 100644 index 0000000..e6a0878 --- /dev/null +++ b/addons/gdUnit4/test/spy/resources/TestSceneWithProperties.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=3 uid="uid://bm5kdwn5suu1t"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/spy/resources/test_scene_with_parmeters.gd" id="1_0ll7j"] + +[node name="TestSceneWithParmeters" type="Node2D"] +script = ExtResource("1_0ll7j") + +[node name="ProgressBar" type="ProgressBar" parent="."] +unique_name_in_owner = true +offset_right = 4.0 +offset_bottom = 27.0 diff --git a/addons/gdUnit4/test/spy/resources/test_scene_with_parmeters.gd b/addons/gdUnit4/test/spy/resources/test_scene_with_parmeters.gd new file mode 100644 index 0000000..bd2188c --- /dev/null +++ b/addons/gdUnit4/test/spy/resources/test_scene_with_parmeters.gd @@ -0,0 +1,15 @@ +class_name TestSceneWithProperties +extends Node2D + + +@onready var progress := %ProgressBar + +@warning_ignore("unused_private_class_variable") +var _parameter_obj := RefCounted.new() +@warning_ignore("unused_private_class_variable") +var _parameter_dict := {"key" : "value"} + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + #prints(progress, _parameter_obj, _parameter_dict) + pass diff --git a/addons/gdUnit4/test/spy/resources/test_scene_with_parmeters.gd.uid b/addons/gdUnit4/test/spy/resources/test_scene_with_parmeters.gd.uid new file mode 100644 index 0000000..b206eef --- /dev/null +++ b/addons/gdUnit4/test/spy/resources/test_scene_with_parmeters.gd.uid @@ -0,0 +1 @@ +uid://frgoi4oqlehr diff --git a/addons/gdUnit4/test/ui/GdUnitUiToolsTest.gd b/addons/gdUnit4/test/ui/GdUnitUiToolsTest.gd new file mode 100644 index 0000000..fc79f75 --- /dev/null +++ b/addons/gdUnit4/test/ui/GdUnitUiToolsTest.gd @@ -0,0 +1,40 @@ +# GdUnit generated TestSuite +class_name GdUnitUiToolsTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/GdUnitUiTools.gd' + + +func test__merge_images_same_size() -> void: + var image_a := Image.create(16, 16, false, Image.FORMAT_RGBA8) + var image_b := Image.create(16, 16, false, Image.FORMAT_RGBA8) + var output := GdUnitUiTools._merge_images(image_a, Vector2.ZERO, image_b, Vector2.ZERO) + + assert_that(output.get_size()).is_equal(Vector2i(16, 16)) + + +func test__merge_images_different_size() -> void: + var image_a := Image.create(8, 8, false, Image.FORMAT_RGBA8) + var image_b := Image.create(16, 16, false, Image.FORMAT_RGBA8) + var output := GdUnitUiTools._merge_images(image_a, Vector2.ZERO, image_b, Vector2.ZERO) + + assert_that(output.get_size()).is_equal(Vector2i(16, 16)) + + +func test__merge_images_scaled() -> void: + var image_a := Image.create(16, 16, false, Image.FORMAT_RGBA8) + var image_b := Image.create(16, 16, false, Image.FORMAT_RGBA8) + var output := GdUnitUiTools._merge_images_scaled(image_a, Vector2.ZERO, image_b, Vector2.ZERO) + + assert_that(output.get_size()).is_equal(Vector2i(16, 16)) + + +func test__merge_images_scaled_different_size() -> void: + var image_a := Image.create(8, 8, false, Image.FORMAT_RGBA8) + var image_b := Image.create(16, 16, false, Image.FORMAT_RGBA8) + var output := GdUnitUiTools._merge_images_scaled(image_a, Vector2.ZERO, image_b, Vector2.ZERO) + + assert_that(output.get_size()).is_equal(Vector2i(16, 16)) diff --git a/addons/gdUnit4/test/ui/GdUnitUiToolsTest.gd.uid b/addons/gdUnit4/test/ui/GdUnitUiToolsTest.gd.uid new file mode 100644 index 0000000..3485a4b --- /dev/null +++ b/addons/gdUnit4/test/ui/GdUnitUiToolsTest.gd.uid @@ -0,0 +1 @@ +uid://cq7sqyuimvbwg diff --git a/addons/gdUnit4/test/ui/parts/GdUnitInspectorProgressBarTest.gd b/addons/gdUnit4/test/ui/parts/GdUnitInspectorProgressBarTest.gd new file mode 100644 index 0000000..4a611bf --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitInspectorProgressBarTest.gd @@ -0,0 +1,41 @@ +extends GdUnitTestSuite + + +const GdUnitInspectorProgressBar := preload("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.gd") + + +var _progress: GdUnitInspectorProgressBar +var _progress_counter: Label +var _style: StyleBoxFlat + + +func before_test() -> void: + @warning_ignore("unsafe_method_access") + var control: Control = load('res://addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.tscn').instantiate() + add_child(control) + _progress = control.find_child("ProgressBar", true, false) + + _progress_counter = _progress.progress_counter + _style = _progress.style + + +func test_progress_init() -> void: + assert_that(_progress.value).is_equal(0.000000) + assert_that(_progress.max_value).is_equal(0.000000) + assert_that(_progress_counter.text).is_equal("0:0") + + +func test_progress_on_test_counter_changed(index: int, total_count: int, state: GdUnitInspectorTreeConstants.STATE, expected_color: Color, _test_parameters := [ + [0, 0, GdUnitInspectorTreeConstants.STATE.INITIAL, Color.TRANSPARENT], + [1, 2, GdUnitInspectorTreeConstants.STATE.SUCCESS, Color.DARK_GREEN], + [2, 2, GdUnitInspectorTreeConstants.STATE.WARNING, Color.DARK_GREEN], + [3, 5, GdUnitInspectorTreeConstants.STATE.RUNNING, Color.DARK_GREEN], + [4, 5, GdUnitInspectorTreeConstants.STATE.FAILED, Color.DARK_RED], + [5, 5, GdUnitInspectorTreeConstants.STATE.ERROR, Color.DARK_RED], +]) -> void: + + _progress._on_test_counter_changed(index, total_count, state) + assert_float(_progress.value).is_equal(index as float) + assert_float(_progress.max_value).is_equal(total_count as float) + assert_str(_progress_counter.text).is_equal("%d:%d" % [index, total_count]) + assert_that(_style.bg_color).is_equal(expected_color) diff --git a/addons/gdUnit4/test/ui/parts/GdUnitInspectorProgressBarTest.gd.uid b/addons/gdUnit4/test/ui/parts/GdUnitInspectorProgressBarTest.gd.uid new file mode 100644 index 0000000..0140ef4 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitInspectorProgressBarTest.gd.uid @@ -0,0 +1 @@ +uid://cgpbbafty7hfn diff --git a/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelPerformanceTest.gd b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelPerformanceTest.gd new file mode 100644 index 0000000..bdf8cef --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelPerformanceTest.gd @@ -0,0 +1,168 @@ +# GdUnit generated TestSuite +class_name GdUnitInspectorTreeMainPanelPerformanceTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd' + +# this test-suite contains only empty test to run as performance indicator + + +func test_01() -> void: + pass + + +func test_02() -> void: + pass + + +func test_03() -> void: + pass + + +func test_04() -> void: + pass + + +func test_05() -> void: + pass + + +func test_06() -> void: + pass + + +func test_07() -> void: + pass + + +func test_08() -> void: + pass + + +func test_09() -> void: + pass + + +func test_10() -> void: + pass + + +func test_11() -> void: + pass + + +func test_12() -> void: + pass + + +func test_13() -> void: + pass + + +func test_14() -> void: + pass + + +func test_15() -> void: + pass + + +func test_16() -> void: + pass + + +func test_17() -> void: + pass + + +func test_18() -> void: + pass + + +func test_19() -> void: + pass + + +func test_20() -> void: + pass + + +func test_21() -> void: + pass + + +func test_22() -> void: + pass + + +func test_23() -> void: + pass + + +func test_24() -> void: + pass + + +func test_25() -> void: + pass + + +func test_26() -> void: + pass + + +func test_27() -> void: + pass + + +func test_28() -> void: + pass + + +func test_29() -> void: + pass + + +func test_30() -> void: + pass + + +func test_31() -> void: + pass + + +func test_32() -> void: + pass + + +func test_33() -> void: + pass + + +func test_34() -> void: + pass + + +func test_35() -> void: + pass + + +func test_36() -> void: + pass + + +func test_37() -> void: + pass + + +func test_38() -> void: + pass + + +func test_39() -> void: + pass + + +func test_40() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelPerformanceTest.gd.uid b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelPerformanceTest.gd.uid new file mode 100644 index 0000000..34cdca3 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelPerformanceTest.gd.uid @@ -0,0 +1 @@ +uid://d0kb7hdin50tj diff --git a/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd new file mode 100644 index 0000000..8ba8132 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd @@ -0,0 +1,824 @@ +# GdUnit generated TestSuite +class_name GdUnitInspectorTreeMainPanelTest +extends GdUnitTestSuite + +# TestSuite generated from +const GdUnitInspectorTreeMainPanel := preload('res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.gd') + +const FAILED := GdUnitInspectorTreeMainPanel.STATE.FAILED +const ERROR := GdUnitInspectorTreeMainPanel.STATE.ERROR +const FLAKY := GdUnitInspectorTreeMainPanel.STATE.FLAKY + + +const META_SCRIPT_PATH := "script_path" + +var suite_a_item: TreeItem +var suite_b_item: TreeItem +var suite_c_item: TreeItem + +var discovered_tests_suite_a := {} +var discovered_tests_suite_b := {} +var discovered_tests_suite_c := {} + + +var _inspector: GdUnitInspectorTreeMainPanel + + +func before_test() -> void: + @warning_ignore("unsafe_method_access") + _inspector = load("res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.tscn").instantiate() + _inspector.disable_test_recovery() + add_child(_inspector) + _inspector.init_tree() + setup_example_tree() + + +func after_test() -> void: + _inspector.cleanup_tree() + remove_child(_inspector) + _inspector.free() + + +func setup_example_tree() -> void: + # load a testsuite + setup_test_env() + + # verify no failures are exists + assert_array(_inspector._on_select_next_item_by_state(FAILED)).is_null() + + +func discover_sink(test_case: GdUnitTestCase) -> void: + _inspector.on_test_case_discover_added(test_case) + + +func setup_test_env() -> void: + var suite_a := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource", true) + var suite_b := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource", true) + var suite_c := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource", true) + + + GdUnitTestDiscoverer.discover_tests(suite_a, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests_suite_a[discover_test.test_name] = discover_test + ) + GdUnitTestDiscoverer.discover_tests(suite_b, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests_suite_b[discover_test.test_name] = discover_test + ) + GdUnitTestDiscoverer.discover_tests(suite_c, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests_suite_c[discover_test.test_name] = discover_test + ) + + suite_a_item = _inspector._find_tree_item_by_test_suite(_inspector._tree_root, suite_a.resource_path, "ExampleTestSuiteA") + suite_b_item = _inspector._find_tree_item_by_test_suite(_inspector._tree_root, suite_b.resource_path, "ExampleTestSuiteB") + suite_c_item = _inspector._find_tree_item_by_test_suite(_inspector._tree_root, suite_c.resource_path, "ExampleTestSuiteC") + + +func set_test_state(test_cases: Array[GdUnitTestCase], state: GdUnitInspectorTreeMainPanel.STATE) -> void: + for test in test_cases: + var item := _inspector._find_tree_item_by_id(_inspector._tree_root, test.guid) + var parent := item.get_parent() + var test_event := GdUnitEvent.new().test_after(test.guid, "test") + match state: + ERROR: + _inspector.set_state_error(parent) + _inspector.set_state_error(item) + FAILED: + _inspector.set_state_failed(parent, test_event) + _inspector.set_state_failed(item, test_event) + FLAKY: + _inspector.set_state_flaky(parent, test_event) + _inspector.set_state_flaky(item, test_event) + + + # _inspector.set_state_succeded(item) + + +func get_item_state(parent :TreeItem, item_name :String = "") -> int: + for item in parent.get_children(): + if item.get_text(0) == item_name: + return GdUnitInspectorTreeMainPanel.get_item_state(item)[0] + return GdUnitInspectorTreeMainPanel.get_item_state(parent)[0] + + +func test_find_item_by_id() -> void: + var suite_script := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource", true) + var discovered_tests := {} + GdUnitTestDiscoverer.discover_tests(suite_script, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests[discover_test.test_name] = discover_test + ) + var test_aa: GdUnitTestCase = discovered_tests["test_aa"] + var item := _inspector._find_tree_item_by_id(_inspector._tree_root, test_aa.guid) + assert_object(item).is_not_null() + + +# Tests a special case, the test is named equal to the test suite +func test_find_item_by_id_GD809() -> void: + var suite_script := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/ui/parts/resources/gd_809/test_example.resource") + var discovered_tests := {} + GdUnitTestDiscoverer.discover_tests(suite_script, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests[discover_test.test_name] = discover_test + ) + assert_dict(discovered_tests).contains_keys(["test_example", "test_example_b"]) + var test_aa: GdUnitTestCase = discovered_tests["test_example"] + var item := _inspector._find_tree_item_by_id(_inspector._tree_root, test_aa.guid) + assert_object(item).is_not_null() + + +# Tests a special case, the test is named equal to the test suite +func test_find_item_by_path_GD809() -> void: + var suite_script := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/ui/parts/resources/gd_809/test_example.resource") + var discovered_tests := {} + GdUnitTestDiscoverer.discover_tests(suite_script, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests[discover_test.test_name] = discover_test + ) + assert_dict(discovered_tests).contains_keys(["test_example", "test_example_b"]) + var test_example: GdUnitTestCase = discovered_tests["test_example"] + var test_example_b: GdUnitTestCase = discovered_tests["test_example"] + # find test_suite by path + var item := _inspector._find_tree_item_by_test_suite(_inspector._tree_root, test_example.suite_resource_path, "test_example") + assert_object(item).is_not_null() + # find tests by id + assert_object(_inspector._find_tree_item_by_id(_inspector._tree_root, test_example.guid)).is_not_null() + assert_object(_inspector._find_tree_item_by_id(_inspector._tree_root, test_example_b.guid)).is_not_null() + + +func test_select_first_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # we have no failures or errors + _inspector._on_select_next_item_by_state(FAILED) + assert_object(_inspector._tree.get_selected()).is_null() + + # add failures + set_test_state([ + discovered_tests_suite_a["test_aa"], + discovered_tests_suite_a["test_ad"], + discovered_tests_suite_c["test_cb"], + discovered_tests_suite_c["test_cc"], + discovered_tests_suite_c["test_ce"]] + , FAILED) + + # select first failure + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + + +func test_select_last_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # we have no failures or errors + _inspector._on_select_previous_item_by_state(FAILED) + assert_object(_inspector._tree.get_selected()).is_null() + + # add failures + set_test_state([ + discovered_tests_suite_a["test_aa"], + discovered_tests_suite_a["test_ad"], + discovered_tests_suite_c["test_cb"], + discovered_tests_suite_c["test_cc"], + discovered_tests_suite_c["test_ce"]] + , FAILED) + # select last failure + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + + +func test_select_next_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # first time select next but no failure exists + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected()).is_null() + + # add failures + set_test_state([ + discovered_tests_suite_a["test_aa"], + discovered_tests_suite_a["test_ad"], + discovered_tests_suite_c["test_cb"], + discovered_tests_suite_c["test_cc"], + discovered_tests_suite_c["test_ce"]] + , FAILED) + + # first time select next than select first failure + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ad") + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cb") + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + # if current last failure selected than select first as next + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + _inspector._on_select_next_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ad") + + +func test_select_previous_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # first time select previous but no failure exists + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected()).is_null() + + # add failures + set_test_state([ + discovered_tests_suite_a["test_aa"], + discovered_tests_suite_a["test_ad"], + discovered_tests_suite_c["test_cb"], + discovered_tests_suite_c["test_cc"], + discovered_tests_suite_c["test_ce"]] + , FAILED) + + # first time select previous than select last failure + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cb") + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ad") + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + # if current first failure selected than select last as next + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + _inspector._on_select_previous_item_by_state(FAILED) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + + +func test_select_next_flaky() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # try select next but no flaky exists + _inspector._on_select_next_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected()).is_null() + + # add flaky tests + set_test_state([ + discovered_tests_suite_c["test_cb"], + discovered_tests_suite_c["test_cc"], + discovered_tests_suite_c["test_ce"]] + , FLAKY) + + # first time select next than select first failure + _inspector._on_select_next_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cb") + _inspector._on_select_next_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + _inspector._on_select_next_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + # if current last failure selected than select first as next + _inspector._on_select_next_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cb") + _inspector._on_select_next_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + + +func test_select_previous_flaky() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # try select previous but no flaky exists + _inspector._on_select_previous_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected()).is_null() + + # add failures + set_test_state([ + discovered_tests_suite_c["test_cb"], + discovered_tests_suite_c["test_cc"], + discovered_tests_suite_c["test_ce"]] + , FLAKY) + + # first time select previous than select last failure + _inspector._on_select_previous_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + _inspector._on_select_previous_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + _inspector._on_select_previous_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cb") + # if current first failure selected than select last as next + _inspector._on_select_previous_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + _inspector._on_select_previous_item_by_state(FLAKY) + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + + +func test_suite_text_shows_amount_of_cases() -> void: + assert_str(suite_a_item.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_str(suite_b_item.get_text(0)).is_equal("(0/3) ExampleTestSuiteB") + + +func test_suite_text_responds_to_test_case_events() -> void: + + var test_aa: GdUnitTestCase = discovered_tests_suite_a["test_aa"] + var test_ab: GdUnitTestCase = discovered_tests_suite_a["test_ab"] + var test_ac: GdUnitTestCase = discovered_tests_suite_a["test_ac"] + var test_ad: GdUnitTestCase = discovered_tests_suite_a["test_ad"] + var test_ae: GdUnitTestCase = discovered_tests_suite_a["test_ae"] + var success_aa := GdUnitEvent.new().test_after(test_aa.guid, "test_aa") + _inspector._on_gdunit_event(success_aa) + assert_str(suite_a_item.get_text(0)).is_equal("(1/5) ExampleTestSuiteA") + + var error_ad := GdUnitEvent.new().test_after(test_ad.guid, "test_ad", {GdUnitEvent.ERRORS: true}) + _inspector._on_gdunit_event(error_ad) + assert_str(suite_a_item.get_text(0)).is_equal("(1/5) ExampleTestSuiteA") + + var failure_ab := GdUnitEvent.new().test_after(test_ab.guid, "test_ab", {GdUnitEvent.FAILED: true}) + _inspector._on_gdunit_event(failure_ab) + assert_str(suite_a_item.get_text(0)).is_equal("(1/5) ExampleTestSuiteA") + + var skipped_ac := GdUnitEvent.new().test_after(test_ac.guid, "test_ac", {GdUnitEvent.SKIPPED: true}) + _inspector._on_gdunit_event(skipped_ac) + assert_str(suite_a_item.get_text(0)).is_equal("(2/5) ExampleTestSuiteA") + + var success_ae := GdUnitEvent.new().test_after(test_ae.guid, "test_ae") + _inspector._on_gdunit_event(success_ae) + assert_str(suite_a_item.get_text(0)).is_equal("(3/5) ExampleTestSuiteA") + + +# test coverage for issue GD-117 +func test_update_test_case_on_multiple_test_suite_with_same_name() -> void: + # add a second test suite where has same name as suite_a_item + var suite_script := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource", true) + var discovered_tests := {} + GdUnitTestDiscoverer.discover_tests(suite_script, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests[discover_test.test_name] = discover_test + ) + var suite_item := _inspector._find_tree_item_by_test_suite(_inspector._tree_root, suite_script.resource_path, "ExampleTestSuiteA") + assert_object(suite_item).is_not_same(suite_a_item) + + # verify inital state + assert_str(suite_item.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_item, "test_aa")).is_equal(_inspector.STATE.INITIAL) + assert_str(suite_a_item.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + + # set test starting checked suite_a_item + var test_aa: GdUnitTestCase = discovered_tests["test_aa"] + var test_ab: GdUnitTestCase = discovered_tests["test_ab"] + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(test_aa.guid)) + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(test_ab.guid)) + assert_str(suite_item.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_item, "test_aa")).is_equal(_inspector.STATE.RUNNING) + assert_int(get_item_state(suite_item, "test_ab")).is_equal(_inspector.STATE.RUNNING) + # test suite_a_item is not affected + assert_str(suite_a_item.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_a_item, "test_aa")).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite_a_item, "test_ab")).is_equal(_inspector.STATE.INITIAL) + + # finish the tests with success + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(test_aa.guid, "test_aa")) + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(test_ab.guid, "test_bb")) + + # verify updated state checked suite_a_item + assert_str(suite_item.get_text(0)).is_equal("(2/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_item, "test_aa")).is_equal(_inspector.STATE.SUCCESS) + assert_int(get_item_state(suite_item, "test_ab")).is_equal(_inspector.STATE.SUCCESS) + # test suite_a_item is not affected + assert_str(suite_a_item.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_a_item, "test_aa")).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite_a_item, "test_ab")).is_equal(_inspector.STATE.INITIAL) + + +# Test coverage for issue GD-278: GdUnit Inspector: Test marks as passed if both warning and error +func test_update_icon_state() -> void: + var suite_script := GdUnitTestResourceLoader.load_gd_script("res://addons/gdUnit4/test/core/execution/resources/orphans/TestSuiteFailAndOrpahnsDetected.gd", true) + var discovered_tests := {} + GdUnitTestDiscoverer.discover_tests(suite_script, func(discover_test: GdUnitTestCase) -> void: + discover_sink(discover_test) + discovered_tests[discover_test.test_name] = discover_test + ) + var suite_script_path := suite_script.resource_path + var suite_name := "TestSuiteFailAndOrpahnsDetected" + var suite_item := _inspector._find_tree_item_by_test_suite(_inspector._tree_root, suite_script_path, suite_name) + + # Verify the inital state + assert_str(suite_item.get_text(0)).is_equal("(0/2) " + suite_name) + assert_int(get_item_state(suite_item)).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite_item, "test_case1")).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite_item, "test_case2")).is_equal(_inspector.STATE.INITIAL) + + # Set tests to running + var test_case1: GdUnitTestCase = discovered_tests["test_case1"] + var test_case2: GdUnitTestCase = discovered_tests["test_case2"] + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(test_case1.guid)) + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(test_case2.guid)) + # Verify all items on state running. + assert_str(suite_item.get_text(0)).is_equal("(0/2) " + suite_name) + assert_int(get_item_state(suite_item, "test_case1")).is_equal(_inspector.STATE.RUNNING) + assert_int(get_item_state(suite_item, "test_case2")).is_equal(_inspector.STATE.RUNNING) + + # Simulate test processed and fails on test_case2 + # test_case1 succeeded + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(test_case1.guid, "test_case1")) + # test_case2 is failing by an orphan warning and an failure + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(test_case2.guid, "test_case2", {GdUnitEvent.FAILED: true})) + # We check whether a test event with a warning does not overwrite a higher object status, e.g. an error. + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(test_case2.guid, "test_case2", {GdUnitEvent.WARNINGS: true})) + + # Verify the final state + assert_str(suite_item.get_text(0)).is_equal("(2/2) " + suite_name) + assert_int(get_item_state(suite_item)).is_equal(_inspector.STATE.FAILED) + assert_int(get_item_state(suite_item, "test_case1")).is_equal(_inspector.STATE.SUCCESS) + assert_int(get_item_state(suite_item, "test_case2")).is_equal(_inspector.STATE.FAILED) + + +func test_tree_view_mode_tree() -> void: + var root: TreeItem = _inspector._tree_root + + var childs := root.get_children() + assert_array(childs).extract("get_text", [0]).contains_exactly(["(0/13) ui"]) + + +func test_custom_sort_by_original_index() -> void: + var tree :Tree = auto_free(Tree.new()) + var tree_root := tree.create_item() + + create_folder_item(tree_root, "folder_a") + create_test_item(tree_root, "test_a") + create_test_item(tree_root, "test_b") + create_folder_item(tree_root, "folder_b") + create_test_item(tree_root, "test_c") + + for n in 10: + # suffle to have a random order + tree_root.get_children().shuffle() + # sort it by original index + GdUnitInspectorTreeMainPanel._sort_tree_items(tree_root, GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED) + # verify + assert_array(tree_root.get_children()).extractv(ItemNameExtractor.new()).contains_exactly([ + # folders should be always on top + "folder_a", "folder_b", + "test_a", "test_b", "test_c"]) + if is_failure(): + break + + +func test_custom_sort_by_name_ascending() -> void: + var tree :Tree = auto_free(Tree.new()) + var tree_root := tree.create_item() + + create_folder_item(tree_root, "folder_a") + create_test_item(tree_root, "test_a") + create_test_item(tree_root, "test_b") + create_folder_item(tree_root, "folder_b") + create_test_item(tree_root, "test_c") + + for n in 10: + # suffle to have a random order + tree_root.get_children().shuffle() + # sort it by name ascending + GdUnitInspectorTreeMainPanel._sort_tree_items(tree_root, GdUnitInspectorTreeConstants.SORT_MODE.NAME_ASCENDING) + # verify + assert_array(tree_root.get_children()).extractv(ItemNameExtractor.new()).contains_exactly([ + # folders should be always on top + "folder_a", "folder_b", + "test_a", "test_b", "test_c"]) + if is_failure(): + break + + +func test_custom_sort_by_name_descending() -> void: + var tree :Tree = auto_free(Tree.new()) + var tree_root := tree.create_item() + + create_folder_item(tree_root, "folder_a") + create_test_item(tree_root, "test_a") + create_test_item(tree_root, "test_b") + create_folder_item(tree_root, "folder_b") + create_test_item(tree_root, "test_c") + + for n in 10: + # suffle to have a random order + tree_root.get_children().shuffle() + # sort it by name descending + GdUnitInspectorTreeMainPanel._sort_tree_items(tree_root, GdUnitInspectorTreeConstants.SORT_MODE.NAME_DESCENDING) + # verify + assert_array(tree_root.get_children()).extractv(ItemNameExtractor.new()).contains_exactly([ + # folders should be always on top + "folder_b", "folder_a", + "test_c", "test_b", "test_a"]) + if is_failure(): + break + +func test_custom_sort_by_execution_time() -> void: + var tree :Tree = auto_free(Tree.new()) + var tree_root := tree.create_item() + + create_folder_item(tree_root, "folder_a", 1000) + create_test_item(tree_root, "test_a", 500) + create_test_item(tree_root, "test_b", 600) + create_folder_item(tree_root, "folder_b", 1500) + create_test_item(tree_root, "test_c", 300) + + for n in 10: + # suffle to have a random order + tree_root.get_children().shuffle() + # sort it by execution time + GdUnitInspectorTreeMainPanel._sort_tree_items(tree_root, GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME) + # verify + assert_array(tree_root.get_children()).extractv(ItemNameExtractor.new()).contains_exactly([ + # folders should be always on top + "folder_b", "folder_a", + "test_b", "test_a", "test_c"]) + if is_failure(): + break + + +func create_test_item(parent: TreeItem, test_name: String, execution_time := 0 ) -> TreeItem: + var item := parent.create_child() + var index := parent.get_child_count() + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_TYPE, GdUnitInspectorTreeMainPanel.GdUnitType.TEST_CASE) + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_NAME, test_name) + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_ORIGINAL_INDEX, index) + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_EXECUTION_TIME, execution_time) + return item + + +func create_folder_item(parent: TreeItem, folder_name: String, execution_time := 0) -> TreeItem: + var item := parent.create_child() + var index := parent.get_child_count() + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_TYPE, GdUnitInspectorTreeMainPanel.GdUnitType.FOLDER) + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_NAME, folder_name) + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_ORIGINAL_INDEX, index) + item.set_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_EXECUTION_TIME, execution_time) + return item + +func test_discover_tests() -> void: + # verify the InspectorProgressBar is connected to gdunit_test_discovered signal + assert_bool(GdUnitSignals.instance().gdunit_test_discover_added.is_connected(_inspector.on_test_case_discover_added))\ + .override_failure_message("The 'InspectorProgressBar' must be connected to signal 'gdunit_test_discovered'")\ + .is_true() + + +func test_on_test_case_discover_added() -> void: + _inspector.init_tree() + _inspector.on_test_case_discover_added(GdUnitTestCase.from("res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", "res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", 0, "test_foo")) + _inspector.on_test_case_discover_added(GdUnitTestCase.from("res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", "res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", 0, "test_bar")) + _inspector.on_test_case_discover_added(GdUnitTestCase.from("res://addons/gdUnit4/test/dir_a/dir_x/my_test_suite2.gd", "res://addons/gdUnit4/test/dir_a/dir_x/my_test_suite2.gd", 0, "test_foo")) + _inspector.on_test_case_discover_added(GdUnitTestCase.from("res://my_test_suite3.gd", "res://my_test_suite3.gd", 0, "test_foo")) + + # create expected tree + var tree: Tree = auto_free(Tree.new()) + var expected_root := tree.create_item() + expected_root.set_text(0, "tree_root") + expected_root.set_meta("gdUnit_name", "tree_root") + var dir_a := create_child(expected_root, "(0/3) dir_a") + var dir_b := create_child(dir_a, "(0/2) dir_b") + var my_test_suite := create_child(dir_b, "(0/2) my_test_suite") + create_child(my_test_suite, "test_foo") + create_child(my_test_suite, "test_bar") + var dir_x := create_child(dir_a, "(0/1) dir_x") + var my_test_suite2 := create_child(dir_x, "(0/1) my_test_suite2") + create_child(my_test_suite2, "test_foo") + var my_test_suite3 := create_child(expected_root, "(0/1) my_test_suite3") + create_child(my_test_suite3, "test_foo") + + assert_tree_equals(_inspector._tree_root, expected_root) + + +func test_add_parameterized_test_case() -> void: + _inspector.init_tree() + _inspector.on_test_case_discover_added(GdUnitTestCase.from("res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", "res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", 0, "test_parameterized", 0, "1.2")) + _inspector.on_test_case_discover_added(GdUnitTestCase.from("res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", "res://addons/gdUnit4/test/dir_a/dir_b/my_test_suite.gd", 0, "test_parameterized", 1, "2.2")) + + # create expected tree + var tree: Tree = auto_free(Tree.new()) + var expected_root := tree.create_item() + expected_root.set_text(0, "tree_root") + expected_root.set_meta("gdUnit_name", "tree_root") + var dir_a := create_child(expected_root, "(0/2) dir_a") + var dir_b := create_child(dir_a, "(0/2) dir_b") + var my_test_suite := create_child(dir_b, "(0/2) my_test_suite") + var test_parameterized := create_child(my_test_suite, "(0/2) test_parameterized") + create_child(test_parameterized, "test_parameterized:0 (1.2)") + create_child(test_parameterized, "test_parameterized:1 (2.2)") + + assert_tree_equals(_inspector._tree_root, expected_root) + + +func test_collect_test_cases() -> void: + var script := load_non_cached("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd") + var tests_by_id := {} + GdUnitTestDiscoverer.discover_tests(script, func(test_to_discover: GdUnitTestCase) -> void: + discover_sink(test_to_discover) + tests_by_id[test_to_discover.display_name] = test_to_discover + ) + + var test_case1: GdUnitTestCase = tests_by_id["test_case1"] + + # Test select a single suite + # Collect all test cases from the suite node (parent of test_case1) + var test := _inspector._find_tree_item_by_id(_inspector._tree_root, test_case1.guid) + var collected_tests := _inspector.collect_test_cases(test.get_parent()) + # Do verify all tests are collected, ignoring the order could be different according to selected sort mode + assert_array(collected_tests).contains_exactly_in_any_order(tests_by_id.values()) + + # Test select a single test + # Find tree node by test id + test = _inspector._find_tree_item_by_id(_inspector._tree_root, test_case1.guid) + # Collect all test cases by given tree node + collected_tests = _inspector.collect_test_cases(test) + assert_array(collected_tests).contains_exactly([test_case1]) + + # Test select on paramaterized + var paramaterized_test: GdUnitTestCase = tests_by_id["test_parameterized_static:0 (1, 1)"] + test = _inspector._find_tree_item_by_id(_inspector._tree_root, paramaterized_test.guid) + # Collect all paramaterized tests (by parent of paramaterized_test) + collected_tests = _inspector.collect_test_cases(test.get_parent()) + # Do verify all tests are collected, ignoring the order could be different according to selected sort mode + var expected_tests: Array = tests_by_id.values().filter(func(test_to_filter: GdUnitTestCase) -> bool: + return test_to_filter.test_name == "test_parameterized_static" + ) + assert_array(collected_tests)\ + .has_size(3)\ + .contains_exactly_in_any_order(expected_tests) + + +func test_collect_test_cases_GD_872(_do_skip := not GdUnit4CSharpApiLoader.is_api_loaded(), _skip_reason := "Do run only for Godot .Net version") -> void: + var tests_by_id := {} + var resource_path := "res://addons/gdUnit4/test/ui/parts/resources/gd_872/" + for suite_path: String in ["ATests.cs", "AZTests.cs"]: + var script := cs_load_non_cached(resource_path + suite_path) + GdUnitTestDiscoverer.discover_tests(script, func(test_to_discover: GdUnitTestCase) -> void: + discover_sink(test_to_discover) + tests_by_id[test_to_discover.fully_qualified_name] = test_to_discover + ) + # Validate if all tests discovered first + assert_dict(tests_by_id)\ + .contains_keys([ + "Minimal.Tests.ATests.TestExample", + "Minimal.Tests.ATests.TestExample2", + "Minimal.Tests.AZTests.TestExample", + "Minimal.Tests.AZTests.TestExample2"])\ + .has_size(4) + # Validate suite `Minimal.Tests.ATests` + var test_case1: GdUnitTestCase = tests_by_id["Minimal.Tests.ATests.TestExample"] + var test_case2: GdUnitTestCase = tests_by_id["Minimal.Tests.ATests.TestExample2"] + # Validate tree paths + assert_str(buld_test_case_tree_path(test_case1)).is_equal("tree_root/Minimal/Tests/ATests/TestExample") + assert_str(buld_test_case_tree_path(test_case2)).is_equal("tree_root/Minimal/Tests/ATests/TestExample2") + # Valide suite find by resource name + assert_object(_inspector._find_tree_item_by_test_suite(_inspector._tree_root, test_case1.suite_resource_path, "ATests")).is_not_null() + + # Validate suite `Minimal.Tests.AZTests` + test_case1 = tests_by_id["Minimal.Tests.AZTests.TestExample"] + test_case2 = tests_by_id["Minimal.Tests.AZTests.TestExample2"] + # Validate tree paths + assert_str(buld_test_case_tree_path(test_case1)).is_equal("tree_root/Minimal/Tests/AZTests/TestExample") + assert_str(buld_test_case_tree_path(test_case2)).is_equal("tree_root/Minimal/Tests/AZTests/TestExample2") + # Valide suite find by resource name + assert_object(_inspector._find_tree_item_by_test_suite(_inspector._tree_root, test_case1.suite_resource_path, "AZTests")).is_not_null() + +## test helpers to validate two trees +# ------------------------------------------------------------------------------------------------------------------------------------------ + +func buld_test_case_tree_path(test_case: GdUnitTestCase) -> String: + var test := _inspector._find_tree_item_by_id(_inspector._tree_root, test_case.guid) + return _build_tree_path(test) + + +func _build_tree_path(item: TreeItem, tree_path: String = "") -> String: + if item == null: + return tree_path + var part: String = item.get_meta("gdUnit_name") + if not tree_path.is_empty(): + part += "/" + tree_path + return _build_tree_path(item.get_parent(), part) + + +func assert_tree_equals(tree_left :TreeItem, tree_right: TreeItem) -> bool: + var left_childs := tree_left.get_children() + var right_childs := tree_right.get_children() + + assert_that(left_childs.size())\ + .override_failure_message("Expecting same child count %d vs %d on item %s" % [left_childs.size(), right_childs.size(), tree_left.get_meta("gdUnit_name")])\ + .is_equal(right_childs.size()) + + if is_failure(): + return false + + for index in left_childs.size(): + var l := left_childs[index] + var r := right_childs[index] + + assert_that(get_item_name(l))\ + .override_failure_message("Expecting '%s' == '%s'" % [get_item_name(l), get_item_name(r)])\ + .is_equal(get_item_name(r)) + if is_failure(): + _print_tree_up(l) + _print_tree_up(r) + _print_execution_times(tree_left) + _print_execution_times(tree_right) + return false + if not assert_tree_equals(l, r): + return false + return true + + +func _print_execution_times(item: TreeItem) -> void: + for child in item.get_children(): + prints(get_item_name(child), get_item_execution_time(child)) + prints("_________________________________________________") + + +func _print_tree(tree_left :TreeItem, indent: String = "\t") -> void: + var left := tree_left.get_children() + for index in left.size(): + var l: TreeItem = left[index] + var state_value: int = l.get_meta(_inspector.META_GDUNIT_STATE) + var state :Variant = _inspector.STATE.keys()[state_value] + prints(indent, l.get_meta("gdunit_original_index"), ":", get_item_name(l), state) + _print_tree(l, indent+"\t") + + +func _print_tree_up(item :TreeItem, indent: String = "\t") -> void: + prints(indent, get_item_name(item), "index:", item.get_meta("gdunit_original_index")) + var parent := item.get_parent() + if parent != null: + _print_tree_up(parent, indent+"\t") + + +func get_item_name(item: TreeItem) -> String: + return item.get_meta("gdUnit_name") + + +func get_item_execution_time(item: TreeItem) -> String: + if item.has_meta("gdUnit_execution_time"): + return "'" + str(item.get_meta("gdUnit_execution_time")) + "'" + return "''" + + +func rebuild_tree_from_resource(resource: String) -> TreeItem: + var json := FileAccess.open(resource, FileAccess.READ) + var dict :Dictionary = JSON.parse_string(json.get_as_text()) + var tree :Tree = auto_free(Tree.new()) + var root := tree.create_item() + var items: Dictionary = dict["TreeItem"] + create_tree_item_form_dict(root, items) + return root + + +func create_tree_item_form_dict(item: TreeItem, data: Dictionary) -> TreeItem: + for key:String in data.keys(): + match key: + "collapsed": + @warning_ignore("unsafe_cast") + item.collapsed = data[key] as bool + + "TreeItem": + var next := item.create_child() + @warning_ignore("unsafe_cast") + return create_tree_item_form_dict(next, data[key] as Dictionary) + + "childrens": + var childs_data :Array = data[key] + for child_data:Dictionary in childs_data: + create_tree_item_form_dict(item, child_data) + + if key.begins_with("metadata"): + var meta_key := key.replace("metadata/", "") + item.set_meta(meta_key, data[key]) + return item + + +func create_child( parent: TreeItem, item_name: String) -> TreeItem: + var item := parent.create_child() + item.set_text(0, item_name) + var regex := RegEx.new() + regex.compile("^\\(\\d+/\\d+\\)\\s*") + var test_name := regex.sub(item_name, "", false) + item.set_meta("gdUnit_name", test_name) + item.collapsed = true + return item + + +# we need to load the scripts freshly uncached because of script changes during test execution +func load_non_cached(resource_path: String) -> GDScript: + return ResourceLoader.load(resource_path, "GDScript", ResourceLoader.CACHE_MODE_IGNORE) + + +func cs_load_non_cached(resource_path: String) -> Script: + return ResourceLoader.load(resource_path, "CSharpScript", ResourceLoader.CACHE_MODE_IGNORE) + + +class ItemNameExtractor extends GdUnitValueExtractor: + + func extract_value(value :Variant) -> Variant: + var item: TreeItem = value + return item.get_meta(GdUnitInspectorTreeMainPanel.META_GDUNIT_NAME) diff --git a/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd.uid b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd.uid new file mode 100644 index 0000000..28832bd --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitInspectorTreeMainPanelTest.gd.uid @@ -0,0 +1 @@ +uid://db22ihjsydvji diff --git a/addons/gdUnit4/test/ui/parts/GdUnitReportPanelTest.gd b/addons/gdUnit4/test/ui/parts/GdUnitReportPanelTest.gd new file mode 100644 index 0000000..78a017d --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitReportPanelTest.gd @@ -0,0 +1,134 @@ +class_name GdUnitReportPanelTest +extends GdUnitTestSuite + + +var _panel: GdUnitReportPanel + + +func before_test() -> void: + @warning_ignore("unsafe_method_access") + _panel = load("res://addons/gdUnit4/src/ui/parts/GdUnitReportPanel.tscn").instantiate() + add_child(_panel) + + +func after_test() -> void: + remove_child(_panel) + _panel.free() + + +func _build_report(message: String) -> GdUnitReport: + return _build_report_with_frames(message, []) + + +func _build_report_with_frames(message: String, frames: Array[GdUnitStackTraceElement]) -> GdUnitReport: + return GdUnitReport.new().from_error( + GdUnitReport.FAILURE, + GdUnitError.new(message, 1, GdUnitStackTrace.of(frames)) + ) + + +#region clear +func test_clear_removes_all_report_children() -> void: + _panel.show_report([_build_report("a"), _build_report("b")]) + _panel.clear() + assert_int(_panel.report_list.get_child_count()).is_equal(0) +#endregion + + +#region show_report +func test_show_report_with_empty_list() -> void: + _panel.show_report([]) + assert_int(_panel.report_list.get_child_count()).is_equal(0) + + +func test_show_report_adds_one_child_per_report() -> void: + var reports: Array[GdUnitReport] = [ + _build_report("failure A"), + _build_report("failure B"), + _build_report("failure C"), + ] + _panel.show_report(reports) + assert_int(_panel.report_list.get_child_count()).is_equal(3) + + +func test_show_report_replaces_previous_reports() -> void: + _panel.show_report([_build_report("first"), _build_report("second")]) + _panel.show_report([_build_report("only")]) + assert_int(_panel.report_list.get_child_count()).is_equal(1) +#endregion + + +#region _on_meta_clicked +func test_meta_clicked_signal_is_connected_when_stack_trace_has_frames() -> void: + var frames: Array[GdUnitStackTraceElement] = [ + GdUnitStackTraceElement.new("res://test/MyTest.gd", 42, "test_foo"), + ] + var label: RichTextLabel = auto_free(_panel.build_report(_build_report_with_frames("test", frames))) + assert_bool(label.meta_clicked.is_connected(_panel._on_meta_clicked)).is_true() + +@warning_ignore_start("redundant_await") +func test_on_meta_clicked_is_called_with_expected_frame_on_click() -> void: + # Use a real source path so GdUnitScriptEditorControls.edit_script can load() the script + var frame1 := GdUnitStackTraceElement.new( + "res://test/MyTest.gd", 42, "test_foo" + ) + # Spy on the panel via the scene so that _on_meta_clicked calls are recorded + var panel_spy: GdUnitReportPanel = spy("res://addons/gdUnit4/src/ui/parts/GdUnitReportPanel.tscn") + var runner := scene_runner(panel_spy) + + # show_report adds the label into the panel's scene tree, giving it a valid rect + panel_spy.show_report([_build_report_with_frames("test", [frame1])]) + await runner.simulate_frames(2) + + # Simulate left mouse click on frame1 + var label: RichTextLabel = panel_spy.report_list.get_child(0) + var label_rect := label.get_global_rect() + # Line 0 is the report message; line 1 is the first stack trace entry + var click_pos := Vector2(label_rect.position.x + 50, label_rect.position.y + label.get_line_offset(1) + 5) + runner.set_mouse_position(click_pos) + await runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + + # Verify _on_meta_clicked was invoked with the expected stack frame + @warning_ignore("unsafe_method_access") + verify(panel_spy)._on_meta_clicked(frame1) + +@warning_ignore_restore("redundant_await") +#endregion + + +#region build_report +func test_build_report_label_is_visible() -> void: + var label: RichTextLabel = auto_free(_panel.build_report(_build_report("test message"))) + assert_bool(label.visible).is_true() + + +func test_build_report_parsed_text_contains_message() -> void: + var label: RichTextLabel = auto_free(_panel.build_report(_build_report("expected failure text"))) + assert_str(label.get_parsed_text()).contains("expected failure text") + + +func test_build_report_with_stack_depth_of_1() -> void: + var frames: Array[GdUnitStackTraceElement] = [ + GdUnitStackTraceElement.new("res://test/MyTest.gd", 42, "test_foo"), + ] + var label: RichTextLabel = auto_free(_panel.build_report(_build_report_with_frames("assertion failed", frames))) + assert_str(label.get_parsed_text()).is_equal(""" + assertion failed + at test_foo in MyTest.gd : 42 + """.dedent().trim_prefix("\n")) + + +func test_build_report_with_stack_depth_of_3() -> void: + var frames: Array[GdUnitStackTraceElement] = [ + GdUnitStackTraceElement.new("res://test/MyTest.gd", 42, "test_foo"), + GdUnitStackTraceElement.new("res://suite/TestSuiteA.gd", 10, "before_test"), + GdUnitStackTraceElement.new("res://runner/TestRunner.gd", 5, "run"), + ] + var label: RichTextLabel = auto_free(_panel.build_report(_build_report_with_frames("assertion failed", frames))) + assert_str(label.get_parsed_text()).is_equal(""" + assertion failed + at test_foo in MyTest.gd : 42 + at before_test in TestSuiteA.gd : 10 + at run in TestRunner.gd : 5 + """.dedent().trim_prefix("\n")) +#endregion diff --git a/addons/gdUnit4/test/ui/parts/GdUnitReportPanelTest.gd.uid b/addons/gdUnit4/test/ui/parts/GdUnitReportPanelTest.gd.uid new file mode 100644 index 0000000..edaf79f --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/GdUnitReportPanelTest.gd.uid @@ -0,0 +1 @@ +uid://4t45l4ss7rtd diff --git a/addons/gdUnit4/test/ui/parts/resources/.gdignore b/addons/gdUnit4/test/ui/parts/resources/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource b/addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource new file mode 100644 index 0000000..f192f7c --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource @@ -0,0 +1,17 @@ +extends GdUnitTestSuite + + +func test_aa() -> void: + pass + +func test_ab() -> void: + pass + +func test_ac() -> void: + pass + +func test_ad() -> void: + pass + +func test_ae() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource new file mode 100644 index 0000000..2019b4b --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource @@ -0,0 +1,18 @@ +class_name ExampleTestSuiteA +extends GdUnitTestSuite + + +func test_aa() -> void: + pass + +func test_ab() -> void: + pass + +func test_ac() -> void: + pass + +func test_ad() -> void: + pass + +func test_ae() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource new file mode 100644 index 0000000..c0bf271 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource @@ -0,0 +1,12 @@ +class_name ExampleTestSuiteB +extends GdUnitTestSuite + + +func test_ba() -> void: + pass + +func test_bb() -> void: + pass + +func test_bc() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource new file mode 100644 index 0000000..e96bc44 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource @@ -0,0 +1,18 @@ +class_name ExampleTestSuiteC +extends GdUnitTestSuite + + +func test_ca() -> void: + pass + +func test_cb() -> void: + pass + +func test_cc() -> void: + pass + +func test_cd() -> void: + pass + +func test_ce() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/gd_809/test_example.resource b/addons/gdUnit4/test/ui/parts/resources/gd_809/test_example.resource new file mode 100644 index 0000000..4747f77 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/gd_809/test_example.resource @@ -0,0 +1,9 @@ +extends GdUnitTestSuite + + +func test_example() -> void: + pass + + +func test_example_b() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/gd_872/ATests.cs b/addons/gdUnit4/test/ui/parts/resources/gd_872/ATests.cs new file mode 100644 index 0000000..4f28c4d --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/gd_872/ATests.cs @@ -0,0 +1,20 @@ +using GdUnit4; + +namespace Minimal.Tests +{ + [TestSuite] + public class ATests + { + [TestCase] + public void TestExample() + { + Assertions.AssertBool(true).IsTrue(); + } + + [TestCase] + public void TestExample2() + { + Assertions.AssertBool(false).IsFalse(); + } + } +} diff --git a/addons/gdUnit4/test/ui/parts/resources/gd_872/AZTests.cs b/addons/gdUnit4/test/ui/parts/resources/gd_872/AZTests.cs new file mode 100644 index 0000000..2dd6b99 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/gd_872/AZTests.cs @@ -0,0 +1,20 @@ +using GdUnit4; + +namespace Minimal.Tests +{ + [TestSuite] + public class AZTests + { + [TestCase] + public void TestExample() + { + Assertions.AssertBool(true).IsTrue(); + } + + [TestCase] + public void TestExample2() + { + Assertions.AssertBool(false).IsFalse(); + } + } +} diff --git a/addons/gdUnit4/test/ui/templates/GdUnitTestSuiteTemplatePanelTest.gd b/addons/gdUnit4/test/ui/templates/GdUnitTestSuiteTemplatePanelTest.gd new file mode 100644 index 0000000..8854e18 --- /dev/null +++ b/addons/gdUnit4/test/ui/templates/GdUnitTestSuiteTemplatePanelTest.gd @@ -0,0 +1,35 @@ +@warning_ignore_start("unsafe_method_access") +# GdUnit generated TestSuite +class_name GdUnitTestSuiteTemplatePanelTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.gd' + + +func test_show() -> void: + var template :Variant = spy("res://addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.tscn") + scene_runner(template) + + # verify the followup functions are called by _ready() + verify(template)._ready() + verify(template).setup_editor_colors() + verify(template).setup_supported_types() + verify(template).load_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + verify(template).setup_tags_help() + + +func test_load_template_gd() -> void: + var runner := scene_runner("res://addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.tscn") + runner.invoke("load_template", GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + + assert_int(runner.get_property("_selected_template")).is_equal(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + assert_str(runner.get_property("_template_editor").text).is_equal(GdUnitTestSuiteTemplate.default_GD_template().replace("\r", "")) + + +func test_load_template_cs() -> void: + var runner := scene_runner("res://addons/gdUnit4/src/ui/templates/GdUnitTestSuiteTemplatePanel.tscn") + runner.invoke("load_template", GdUnitTestSuiteTemplate.TEMPLATE_ID_CS) + + assert_int(runner.get_property("_selected_template")).is_equal(GdUnitTestSuiteTemplate.TEMPLATE_ID_CS) + assert_str(runner.get_property("_template_editor").text).is_equal(GdUnitTestSuiteTemplate.default_CS_template().replace("\r", "")) diff --git a/addons/gdUnit4/test/ui/templates/GdUnitTestSuiteTemplatePanelTest.gd.uid b/addons/gdUnit4/test/ui/templates/GdUnitTestSuiteTemplatePanelTest.gd.uid new file mode 100644 index 0000000..af9d1d0 --- /dev/null +++ b/addons/gdUnit4/test/ui/templates/GdUnitTestSuiteTemplatePanelTest.gd.uid @@ -0,0 +1 @@ +uid://sjkui5cyyh84 diff --git a/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd b/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd new file mode 100644 index 0000000..1964d3e --- /dev/null +++ b/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd @@ -0,0 +1,122 @@ +# GdUnit generated TestSuite +class_name GdMarkDownReaderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/update/GdMarkDownReader.gd' +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +var _reader := preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd").new() +var _client :GdUnitUpdateClient + + +func before() -> void: + _client = GdUnitUpdateClient.new() + add_child(_client) + _reader.set_http_client(_client) + + +func after() -> void: + _client.queue_free() + + +func test_tobbcode() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/markdown_example.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_example.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_table() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/markdown_table.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_table.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_html_headers() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/html_header.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_header.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_md_headers() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/md_header.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_md_header.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_list() -> void: + assert_str(await _reader.to_bbcode("- item")).is_equal("[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] item") + assert_str(await _reader.to_bbcode(" - item")).is_equal(" [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] item") + assert_str(await _reader.to_bbcode(" - item")).is_equal(" [img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] item") + assert_str(await _reader.to_bbcode(" - item")).is_equal(" [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] item") + + +func test_to_bbcode_embeded_text() -> void: + assert_str(await _reader.to_bbcode("> some text")).is_equal("[img=50x14]res://addons/gdUnit4/src/update/assets/embedded.png[/img][i] some text[/i]") + + +func test_process_image() -> void: + #regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)") + var reg_ex :RegEx = _reader.md_replace_patterns[25][0] + + # without tooltip + assert_str(await _reader.process_image(reg_ex, "![alt text](res://addons/gdUnit4/test/update/resources/icon48.png)"))\ + .is_equal("[img]res://addons/gdUnit4/test/update/resources/icon48.png[/img]") + # with tooltip + assert_str(await _reader.process_image(reg_ex, "![alt text](res://addons/gdUnit4/test/update/resources/icon48.png \"Logo Title Text 1\")"))\ + .is_equal("[img]res://addons/gdUnit4/test/update/resources/icon48.png[/img]") + # multiy lines + var input := """ + ![alt text](res://addons/gdUnit4/test/update/resources/icon48.png) + + ![alt text](res://addons/gdUnit4/test/update/resources/icon23.png \"Logo Title Text 1\") + + """.dedent() + var expected := """ + [img]res://addons/gdUnit4/test/update/resources/icon48.png[/img] + + [img]res://addons/gdUnit4/test/update/resources/icon23.png[/img] + + """.dedent() + assert_str(await _reader.process_image(reg_ex, input))\ + .is_equal(expected) + + +func test_process_image_by_reference() -> void: + #regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)") + var reg_ex :RegEx = _reader.md_replace_patterns[24][0] + var input := """ + ![alt text1][logo-1] + + [logo-1]:https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2" + + ![alt text2][logo-1] + + """.dedent() + + var expected := """ + ![alt text1](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png) + + + ![alt text2](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png) + + """.replace("\r", "").dedent() + + # without tooltip + assert_str(_reader.process_image_references(reg_ex, input))\ + .is_equal(expected) + + +func test_process_external_image_save_as_png() -> void: + var input := """ + [img]https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png[/img] + [img]https://github.com/MikeSchulze/gdUnit4/assets/347037/3205c9f1-1746-4716-aa6d-e3a1808b761d[/img] + """.dedent() + + var output := await _reader._process_external_image_resources(input) + assert_str(output).is_equal(""" + [img]res://addons/gdUnit4/tmp-update/icon48.png[/img] + [img]res://addons/gdUnit4/tmp-update/3205c9f1-1746-4716-aa6d-e3a1808b761d.png[/img] + """.dedent()) + assert_file("res://addons/gdUnit4/tmp-update/icon48.png").exists() + assert_file("res://addons/gdUnit4/tmp-update/3205c9f1-1746-4716-aa6d-e3a1808b761d.png").exists() diff --git a/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd.uid b/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd.uid new file mode 100644 index 0000000..f7147ef --- /dev/null +++ b/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd.uid @@ -0,0 +1 @@ +uid://c0qkhw3kvi60k diff --git a/addons/gdUnit4/test/update/GdUnitPatcherTest.gd b/addons/gdUnit4/test/update/GdUnitPatcherTest.gd new file mode 100644 index 0000000..a06d891 --- /dev/null +++ b/addons/gdUnit4/test/update/GdUnitPatcherTest.gd @@ -0,0 +1,114 @@ +# GdUnit generated TestSuite +class_name GdUnitPatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/update/GdUnitPatcher.gd' + +const _patches := "res://addons/gdUnit4/test/update/resources/patches/" + +var _patcher :GdUnitPatcher + + +func before() -> void: + _patcher = auto_free(GdUnitPatcher.new()) + + +func before_test() -> void: + Engine.set_meta(GdUnitPatch.PATCH_VERSION, []) + _patcher._patches.clear() + + +func test__collect_patch_versions_no_patches() -> void: + # using higher version than patches exists in patch folder + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(3,0,0))).is_empty() + + +func test__collect_patch_versions_current_eq_latest_version() -> void: + # using equal version than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(1,1,4))).is_empty() + + +func test__collect_patch_versions_current_lower_latest_version() -> void: + # using one version lower than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(0,9,9)))\ + .contains_exactly(["res://addons/gdUnit4/test/update/resources/patches/v1.1.4"]) + + # using two versions lower than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(0,9,8)))\ + .contains_exactly([ + "res://addons/gdUnit4/test/update/resources/patches/v0.9.9", + "res://addons/gdUnit4/test/update/resources/patches/v1.1.4"]) + + # using three versions lower than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(0,9,5)))\ + .contains_exactly([ + "res://addons/gdUnit4/test/update/resources/patches/v0.9.6", + "res://addons/gdUnit4/test/update/resources/patches/v0.9.9", + "res://addons/gdUnit4/test/update/resources/patches/v1.1.4"]) + + +func test_scan_patches() -> void: + _patcher._scan(_patches, GdUnit4Version.new(0,9,6)) + assert_dict(_patcher._patches)\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v0.9.9", PackedStringArray(["patch_a.gd", "patch_b.gd"]))\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v1.1.4", PackedStringArray(["patch_a.gd"])) + assert_int(_patcher.patch_count()).is_equal(3) + + _patcher._patches.clear() + _patcher._scan(_patches, GdUnit4Version.new(0,9,5)) + assert_dict(_patcher._patches)\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v0.9.6", PackedStringArray(["patch_x.gd"]))\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v0.9.9", PackedStringArray(["patch_a.gd", "patch_b.gd"]))\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v1.1.4", PackedStringArray(["patch_a.gd"])) + assert_int(_patcher.patch_count()).is_equal(4) + + +func test_execute_no_patches() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + + +func test_execute_v_095() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.parse("v0.9.5")) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_equal([ + GdUnit4Version.parse("v0.9.6"), + GdUnit4Version.parse("v0.9.9-a"), + GdUnit4Version.parse("v0.9.9-b"), + GdUnit4Version.parse("v1.1.4"), + ]) + + +func test_execute_v_096() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.parse("v0.9.6")) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_equal([ + GdUnit4Version.parse("v0.9.9-a"), + GdUnit4Version.parse("v0.9.9-b"), + GdUnit4Version.parse("v1.1.4"), + ]) + + +func test_execute_v_099() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.new(0,9,9)) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_equal([ + GdUnit4Version.parse("v1.1.4"), + ]) + + +func test_execute_v_150() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.parse("v1.5.0")) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() diff --git a/addons/gdUnit4/test/update/GdUnitPatcherTest.gd.uid b/addons/gdUnit4/test/update/GdUnitPatcherTest.gd.uid new file mode 100644 index 0000000..7202177 --- /dev/null +++ b/addons/gdUnit4/test/update/GdUnitPatcherTest.gd.uid @@ -0,0 +1 @@ +uid://eoudxgh37opx diff --git a/addons/gdUnit4/test/update/GdUnitUpdateTest.gd b/addons/gdUnit4/test/update/GdUnitUpdateTest.gd new file mode 100644 index 0000000..59ee1f6 --- /dev/null +++ b/addons/gdUnit4/test/update/GdUnitUpdateTest.gd @@ -0,0 +1,44 @@ +# GdUnit generated TestSuite +class_name GdUnitUpdateTest +extends GdUnitTestSuite + +# TestSuite generated from +const GdUnitUpdate = preload('res://addons/gdUnit4/src/update/GdUnitUpdate.gd') + +# Store original content to restore after test execution +const testResource := "res://addons/gdUnit4/test/update/resources/ExampleSceneWithUids.txt" +var original_content: String + +func before() -> void: + var file := FileAccess.open(testResource, FileAccess.READ) + original_content = file.get_as_text() + file.close() + + +func after() -> void: + var file := FileAccess.open(testResource, FileAccess.WRITE) + file.store_string(original_content) + file.close() + +func after_test() -> void: + clean_temp_dir() + + +func test_remove_uids_from_file() -> void: + var update_tool: GdUnitUpdate = auto_free(GdUnitUpdate.new()) + + # start uid patching + update_tool.remove_uids_from_file(testResource) + + # Verify + var patched_content := get_content(testResource) + var expected_content := get_content("res://addons/gdUnit4/test/update/resources/ExampleScenePached.txt") + assert_str(patched_content).is_equal(expected_content) + + +func get_content(resource: String) -> String: + var file := FileAccess.open(resource, FileAccess.READ) + var content := file.get_as_text() + file.close() + + return content diff --git a/addons/gdUnit4/test/update/GdUnitUpdateTest.gd.uid b/addons/gdUnit4/test/update/GdUnitUpdateTest.gd.uid new file mode 100644 index 0000000..88d9e27 --- /dev/null +++ b/addons/gdUnit4/test/update/GdUnitUpdateTest.gd.uid @@ -0,0 +1 @@ +uid://cl333a03d4a8y diff --git a/addons/gdUnit4/test/update/bbcodeView.gd b/addons/gdUnit4/test/update/bbcodeView.gd new file mode 100644 index 0000000..2c1115e --- /dev/null +++ b/addons/gdUnit4/test/update/bbcodeView.gd @@ -0,0 +1,48 @@ +extends Control + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdMarkDownReader := preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd") +const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +@onready var _input: TextEdit = $HSplitContainer/TextEdit +@onready var _text: RichTextLabel = $HSplitContainer/RichTextLabel + +@onready var _update_client :GdUnitUpdateClient = $GdUnitUpdateClient + +var _md_reader := GdMarkDownReader.new() + + +func _ready() -> void: + _md_reader.set_http_client(_update_client) + #GdUnitFonts.init_fonts(_text) + var source := GdUnitFileAccess.resource_as_string("res://addons/gdUnit4/test/update/resources/http_response_releases.txt") + _input.text = source + + await set_bbcode(source) + + +func set_bbcode(text :String) -> void: + var bbcode := await _md_reader.to_bbcode(text) + _text.clear() + _text.append_text(bbcode) + _text.queue_redraw() + + +func _on_TextEdit_text_changed() -> void: + await set_bbcode(_input.get_text()) + + +func _on_RichTextLabel_meta_clicked(meta :String) -> void: + var properties: Dictionary = str_to_var(meta) + if properties.has("url"): + OS.shell_open(str(properties.get("url"))) + + +func _on_RichTextLabel_meta_hover_started(meta :String) -> void: + var properties: Dictionary = str_to_var(meta) + if properties.has("tool_tip"): + _text.set_tooltip_text(str(properties.get("tool_tip"))) + + +func _on_RichTextLabel_meta_hover_ended(_meta :String) -> void: + _text.set_tooltip_text("") diff --git a/addons/gdUnit4/test/update/bbcodeView.gd.uid b/addons/gdUnit4/test/update/bbcodeView.gd.uid new file mode 100644 index 0000000..cedaf95 --- /dev/null +++ b/addons/gdUnit4/test/update/bbcodeView.gd.uid @@ -0,0 +1 @@ +uid://cgi5fqpu8taim diff --git a/addons/gdUnit4/test/update/bbcodeView.tscn b/addons/gdUnit4/test/update/bbcodeView.tscn new file mode 100644 index 0000000..e096c51 --- /dev/null +++ b/addons/gdUnit4/test/update/bbcodeView.tscn @@ -0,0 +1,39 @@ +[gd_scene load_steps=3 format=3 uid="uid://c1rwx6anh3u3m"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/update/bbcodeView.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="6"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") + +[node name="HSplitContainer" type="HSplitContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +split_offset = 600 + +[node name="TextEdit" type="TextEdit" parent="HSplitContainer"] +layout_mode = 2 + +[node name="RichTextLabel" type="RichTextLabel" parent="HSplitContainer"] +use_parent_material = true +layout_mode = 2 +tooltip_text = "test" +bbcode_enabled = true + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("6") + +[connection signal="text_changed" from="HSplitContainer/TextEdit" to="." method="_on_TextEdit_text_changed"] +[connection signal="meta_clicked" from="HSplitContainer/RichTextLabel" to="." method="_on_RichTextLabel_meta_clicked"] +[connection signal="meta_hover_ended" from="HSplitContainer/RichTextLabel" to="." method="_on_RichTextLabel_meta_hover_ended"] +[connection signal="meta_hover_started" from="HSplitContainer/RichTextLabel" to="." method="_on_RichTextLabel_meta_hover_started"] diff --git a/addons/gdUnit4/test/update/resources/ExampleScenePached.txt b/addons/gdUnit4/test/update/resources/ExampleScenePached.txt new file mode 100644 index 0000000..190a656 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/ExampleScenePached.txt @@ -0,0 +1,66 @@ +[gd_scene load_steps=8 format=3 uid="uid://mpo5o6d4uybu"] + +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.tscn" id="1"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.tscn" id="2"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.tscn" id="3"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorMonitor.tscn" id="4"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.tscn" id="7"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] + +[node name="GdUnit" type="Panel"] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 11 +size_flags_vertical = 3 +focus_mode = 2 +script = ExtResource("5") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +use_parent_material = true +clip_contents = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_vertical = 11 + +[node name="Header" type="VBoxContainer" parent="VBoxContainer"] +use_parent_material = true +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 9 + +[node name="ToolBar" parent="VBoxContainer/Header" instance=ExtResource("1")] +layout_mode = 2 +size_flags_vertical = 1 + +[node name="ProgressBar" parent="VBoxContainer/Header" instance=ExtResource("2")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 5 +max_value = 0.0 + +[node name="StatusBar" parent="VBoxContainer/Header" instance=ExtResource("3")] +layout_mode = 2 +size_flags_horizontal = 11 + +[node name="MainPanel" parent="VBoxContainer" instance=ExtResource("7")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Monitor" parent="VBoxContainer" instance=ExtResource("4")] +layout_mode = 2 + +[node name="event_server" parent="." instance=ExtResource("7_721no")] + +[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]] +[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]] +[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]] +[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]] +[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [4]] +[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [4]] +[connection signal="tree_view_mode_changed" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_status_bar_tree_view_mode_changed"] +[connection signal="jump_to_orphan_nodes" from="VBoxContainer/Monitor" to="VBoxContainer/MainPanel" method="select_first_orphan"] diff --git a/addons/gdUnit4/test/update/resources/ExampleSceneWithUids.txt b/addons/gdUnit4/test/update/resources/ExampleSceneWithUids.txt new file mode 100644 index 0000000..190a656 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/ExampleSceneWithUids.txt @@ -0,0 +1,66 @@ +[gd_scene load_steps=8 format=3 uid="uid://mpo5o6d4uybu"] + +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorToolBar.tscn" id="1"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorProgressBar.tscn" id="2"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorStatusBar.tscn" id="3"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorMonitor.tscn" id="4"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/GdUnitInspectorTreeMainPanel.tscn" id="7"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] + +[node name="GdUnit" type="Panel"] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 11 +size_flags_vertical = 3 +focus_mode = 2 +script = ExtResource("5") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +use_parent_material = true +clip_contents = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_vertical = 11 + +[node name="Header" type="VBoxContainer" parent="VBoxContainer"] +use_parent_material = true +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 9 + +[node name="ToolBar" parent="VBoxContainer/Header" instance=ExtResource("1")] +layout_mode = 2 +size_flags_vertical = 1 + +[node name="ProgressBar" parent="VBoxContainer/Header" instance=ExtResource("2")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 5 +max_value = 0.0 + +[node name="StatusBar" parent="VBoxContainer/Header" instance=ExtResource("3")] +layout_mode = 2 +size_flags_horizontal = 11 + +[node name="MainPanel" parent="VBoxContainer" instance=ExtResource("7")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Monitor" parent="VBoxContainer" instance=ExtResource("4")] +layout_mode = 2 + +[node name="event_server" parent="." instance=ExtResource("7_721no")] + +[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]] +[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]] +[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]] +[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]] +[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [4]] +[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [4]] +[connection signal="tree_view_mode_changed" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_status_bar_tree_view_mode_changed"] +[connection signal="jump_to_orphan_nodes" from="VBoxContainer/Monitor" to="VBoxContainer/MainPanel" method="select_first_orphan"] diff --git a/addons/gdUnit4/test/update/resources/bbcode_example.txt b/addons/gdUnit4/test/update/resources/bbcode_example.txt new file mode 100644 index 0000000..d7dc8cf --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_example.txt @@ -0,0 +1,22 @@ +[font_size=20]GdUnit3 v0.9.4 - Release Candidate[/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=18]Improvements[/font_size] +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] Added project settings to configure: +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] [b]Verbose Orphans[/b] to enable/disable report detected orphans +[img]res://addons/gdUnit4/tmp-update/119266895-e09d1900-bbec-11eb-91e9-45409ba2edb2.png[/img] +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] [b]Server Connection Timeout Minites[/b] to set test server connection timeout in minutes +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] [b]Test Timeout Seconds[/b] to set the default test case timeout in seconds +[img]res://addons/gdUnit4/tmp-update/119266875-d1b66680-bbec-11eb-856f-8fac9b0ed31c.png[/img] + +test seperator +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=18]Bugfixes[/font_size] +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] GdUnit inspecor: + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Fixed invalid test case state visualisation for detected orphan nodes (#63) + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Fixed a ui bug to auto select the first report failure after a test run + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Fixed invalid visualisation state and error counter (#66) +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] TestSuite: + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Using asserts on stage after() now reporting +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] Core: + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] The GdUnit network layer was replaced by a new TCP server/client architecture to enable network-related testing (#64 ) diff --git a/addons/gdUnit4/test/update/resources/bbcode_header.txt b/addons/gdUnit4/test/update/resources/bbcode_header.txt new file mode 100644 index 0000000..880df2c --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_header.txt @@ -0,0 +1,21 @@ +[font_size=22]Header 1 Text[/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] +[font_size=22][center]Header 1 Centered Text[/center][/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=20]Header 2 Text[/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] +[font_size=22][center]Header 2 Centered Text[/center][/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=22][center]Header 2 Centered Text +Multiline Test +Is here[/center][/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=18]Header 3 Text[/font_size] +[font_size=18][center]Header 3 Centered Text[/center][/font_size] + +[font_size=16]Header 4 Text[/font_size] +[font_size=16][center]Header 4 Centered Text[/center][/font_size] + +[font_size=14]Header 5 Text[/font_size] +[font_size=14][center]Header 5 Centered Text[/center][/font_size] + +[font_size=12]Header 6 Text[/font_size] +[font_size=12][center]Header 6 Centered Text[/center][/font_size] diff --git a/addons/gdUnit4/test/update/resources/bbcode_md_header.txt b/addons/gdUnit4/test/update/resources/bbcode_md_header.txt new file mode 100644 index 0000000..2b60a1a --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_md_header.txt @@ -0,0 +1,11 @@ +[font_size=22]Header 1 Text[/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=20]Header 2 Text[/font_size][img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=18]Header 3 Text[/font_size] + +[font_size=16]Header 4 Text[/font_size] + +[font_size=14]Header 5 Text[/font_size] + +[font_size=12]Header 6 Text[/font_size] diff --git a/addons/gdUnit4/test/update/resources/bbcode_table.txt b/addons/gdUnit4/test/update/resources/bbcode_table.txt new file mode 100644 index 0000000..eda2c4a --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_table.txt @@ -0,0 +1,8 @@ +[table=2] +[cell][b] type [/b][/cell]|[cell][b] description[/b][/cell] +[cell]--------------[/cell]|[cell]--------------------------------------------[/cell] +[cell]any_color() [/cell]|[cell] Argument matcher to match any Color value[/cell] +[cell]any_vector2() [/cell]|[cell] Argument matcher to match any Vector2 value[/cell] +[cell]any_vector3() [/cell]|[cell] Argument matcher to match any Vector3 value[/cell] +[cell]any_rect2() [/cell]|[cell] Argument matcher to match any Rect2 value[/cell] +[/table] diff --git a/addons/gdUnit4/test/update/resources/html_header.txt b/addons/gdUnit4/test/update/resources/html_header.txt new file mode 100644 index 0000000..d9206b4 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/html_header.txt @@ -0,0 +1,21 @@ +

Header 1 Text

+

Header 1 Centered Text

+ +

Header 2 Text

+

Header 2 Centered Text

+ +

Header 2 Centered Text +Multiline Test +Is here

+ +

Header 3 Text

+

Header 3 Centered Text

+ +

Header 4 Text

+

Header 4 Centered Text

+ +
Header 5 Text
+
Header 5 Centered Text
+ +
Header 6 Text
+
Header 6 Centered Text
diff --git a/addons/gdUnit4/test/update/resources/http_response_releases.txt b/addons/gdUnit4/test/update/resources/http_response_releases.txt new file mode 100644 index 0000000..ad38a6e --- /dev/null +++ b/addons/gdUnit4/test/update/resources/http_response_releases.txt @@ -0,0 +1,164 @@ +Scan for GdUnit4 Update ... +[color=#6495edff]

GdUnit Release v4.4.3

[/color] +# Hotfix memory leaks on test execution + +## What's Changed + +* GD-598: Fixing memory leaks on test execution by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/599 + + +**Full Changelog**: https://github.com/MikeSchulze/gdUnit4/compare/v4.4.2...v4.4.3 + + +[color=#6495edff]

GdUnit Release v4.4.2

[/color] + + +## What's Changed +### Bug Fixes +* GD-592: Fixes, the detection of parameterized test data changes in `TestDiscoveryGuard` to notify the test explorer and update the structure. by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/593 +* GD-594: Fixes, mock fails on class with parameter getter setter by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/595 +* GD-596: Fixes, run test via CMD line is broken when use skipp command option by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/597 + +### Other Changes +* GD-590: Fixes, HTML test report footer overlapping in Firefox by @Eggbertx in https://github.com/MikeSchulze/gdUnit4/pull/589 + + +**Full Changelog**: https://github.com/MikeSchulze/gdUnit4/compare/v4.4.1...v4.4.2 + + +[color=#6495edff]

GdUnit Release v4.4.1

[/color] + + +## What's Changed +### Improvements +* GD-579: Minimize warnings of testsuite resource loading by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/587 +* GD-579: `Part1:` Minimize `unsafe_call_argument` warnings by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/580 +* GD-579: `Part2`: Minimize `return_value_discarded` warnings by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/582 +* GD-579: `Part3:` Minimize `unsafe_property_access` warnings by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/583 +* GD-579: `Part4:` Minimize `unsafe_method_access` warnings by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/584 +* GD-579: `Part5:` Minimize `unsafe_cast` warnings by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/586 + + +**Full Changelog**: https://github.com/MikeSchulze/gdUnit4/compare/v4.4.0...v4.4.1 + + +[color=#6495edff]

GdUnit Release v4.4.0

[/color] +GdUnit4 v4.4.0 + + + +## What's Changed +* Introduction of flaky test detection, automatically executed again if flaky test detection is enabled. +* Added touchscreen support to the SceneRunner. +* HTML report look & feel redesign + +### Improvements +* GD-220: Add support for flaky test handling and retrying their execution by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/558 +![image](https://github.com/user-attachments/assets/fe90f750-e9c6-4ff9-b892-2885f00fd71c) +* GD-554: Adding support for touch screen input event testing to `GdUnitSceneRunner` by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/556 +```gd +## Simulates a screen touch is pressed.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The position to touch the screen.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@warning_ignore("unused_parameter") +func simulate_screen_touch_pressed(index :int, position :Vector2, double_tap := false) -> GdUnitSceneRunner: + + +## Simulates a screen touch is press.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The position to touch the screen.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@warning_ignore("unused_parameter") +func simulate_screen_touch_press(index :int, position :Vector2, double_tap := false) -> GdUnitSceneRunner: + + +## Simulates a screen touch is released.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@warning_ignore("unused_parameter") +func simulate_screen_touch_release(index :int, double_tap := false) -> GdUnitSceneRunner: + + +## Simulates a touch screen drag&drop to the relative coordinates (offset).[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drag&drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member relative] : The relative position, indicating the drag&drop position offset.[br] +## [member time] : The time to move to the relative position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 +## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50)) +## # and drop it at final at 150,50 relative (50,50 + 100,0) +## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0)) +## [/codeblock] +@warning_ignore("unused_parameter") +func simulate_screen_touch_drag_relative(index :int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + + +## Simulates a touch screen drop to the absolute coordinates (offset).[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The final position, indicating the drop position.[br] +## [member time] : The time to move to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 +## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50)) +## # and drop it at 100,50 +## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50)) +## [/codeblock] +@warning_ignore("unused_parameter") +func simulate_screen_touch_drag_absolute(index :int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + + +## Simulates a touch screen drop&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 drag start position, indicating the drag position.[br] +## [member drop_position] : The drop position, indicating the drop position.[br] +## [member time] : The time to move to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 and drop it at 100,50 +## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50)) +## [/codeblock] +@warning_ignore("unused_parameter") +func simulate_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] +@warning_ignore("unused_parameter") +func simulate_screen_touch_drag(index :int, position: Vector2) -> GdUnitSceneRunner: + + +## Returns the actual position of the touch drag postion by given index +## [member index] : The touch index in the case of a multi-touch event.[br] +@warning_ignore("unused_parameter") +func get_screen_touch_drag_position(index: int) -> Vector2: +``` +* GD-566: Update HTML report page look & feel by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/567 +![image](https://github.com/user-attachments/assets/141b5527-168a-4745-91ce-2c8d9112cda1) + + +### Bug Fixes +* GD-549: Fix, error if GdUnit4 inspector tab is floating by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/552 +* GD-559: Fix, inspector directory collapse toggling by @poohcom1 in https://github.com/MikeSchulze/gdUnit4/pull/560 +* GD-563: Fix, parameterized test not executed when using typed arrays end ends with invalid success state by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/572 +* GD-573: Fixed the inspector double-click, inherited tests to jump to the script where the test are located by @MikeSchulze in https://github.com/MikeSchulze/gdUnit4/pull/574 + +## New Contributors +* @poohcom1 made their first contribution in https://github.com/MikeSchulze/gdUnit4/pull/560 + +**Full Changelog**: https://github.com/MikeSchulze/gdUnit4/compare/v4.3.4...v4.4.0 diff --git a/addons/gdUnit4/test/update/resources/icon48.png b/addons/gdUnit4/test/update/resources/icon48.png new file mode 100644 index 0000000..9f8d934 Binary files /dev/null and b/addons/gdUnit4/test/update/resources/icon48.png differ diff --git a/addons/gdUnit4/test/update/resources/icon48.png.import b/addons/gdUnit4/test/update/resources/icon48.png.import new file mode 100644 index 0000000..61445bc --- /dev/null +++ b/addons/gdUnit4/test/update/resources/icon48.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://q8ckfcdvkyc5" +path="res://.godot/imported/icon48.png-e87a81340792ea4239ce79ba4259a0dd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/test/update/resources/icon48.png" +dest_files=["res://.godot/imported/icon48.png-e87a81340792ea4239ce79ba4259a0dd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/test/update/resources/markdown.txt b/addons/gdUnit4/test/update/resources/markdown.txt new file mode 100644 index 0000000..058a8f3 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/markdown.txt @@ -0,0 +1,168 @@ +------ HTML Headers --- +[font_size=24]Header 1 Text[/font_size] +[font_size=24][center]Header 1Centered Text[/center][/font_size] + +[font_size=20]Header 2 Text[/font_size] +[font_size=20][center]Header 2 Centered Text[/center][/font_size] + +[font_size=20][center]Header 2 Centered Text +Multiline Test +Is here[/center][/font_size] + +--- + +------ Markdown ------------- + +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 + + +------ embeded ------ +>This is an **embedded section**. +>The section continues here + +>This is another **embedded section**. +>This section also continues in the second like +>- aba +>This line isnโ€™t embedded any more. +>- tets +> - aha +> - akaka + + + + +------ lists ------ +* an asterisk starts an unordered list +* and this is another item in the list ++ or you can also use the + character +- or the - character + +To start an ordered list, write this: + +1. this starts a list *with* numbers +* this will show as number "2" +* this will show as number "3." +9. any number, +, -, or * will keep the list going. + * just indent by 4 spaces (or tab) to make a sub-list + 1. keep indenting for more sub lists + * here i'm back to the second level + + + +- Asserts: + - Added new `assert_vector2` to verify Vector2 values (#69 ) + - Added new `assert_vector3` to verify Vector3 values (#69 ) + - ahss + - kaka + - kaka + - kaka + - lll + - kkk + + +- Fuzzers: + - Added `rangev2` to generate random Vector2 values + - Added `rangev3` to generate random Vector3 values + - one or more fuzzers are now allowed for a test case (#71) +- GitHub Action + - Added GitHub action to automatic trigger selftest on push events (tests against Godot 3.2.3, 3.3, 3.3.1, 3.3.2) (#74 ) + + + +------ check lists ------ +[ ] A +[x] B +[ ] C + +------ code ------ +This is `code`. + +``This is all `code`.`` + +```javascript +var s = "JavaScript syntax highlighting"; +alert(s); +``` + +```python +s = "Python syntax highlighting" +print s +``` + +``` +No language indicated, so no syntax highlighting. +But let's throw in a tag. +``` + +------ links ------ +Here is a [Link](https://example.com/ "Optional link title"). + +------ image ------ +Inline-style: +![alt text](res://addons/gdUnit4/test/update/resources/icon48.png "Logo Title Text 1") + +![alt text](https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png) + + +Reference-style: +![alt text][logo] + +[logo]:res://addons/gdUnit4/test/update/resources/icon48.png "Logo Title Text 2" + + +------ Horizontal Rules ------ + +--- +Hyphens +*** +Asterisks +___ +Underscores + + + +------ table ------ +|Column 1|Column 2| +|--------|--------| +| A | B | +| C | D | + +Column 1|Column 2 +--------|-------- +A | B +C | D + + +------ foodnodes ------ +You can easily place footnotes [^2] in the continuous text [^1]. +[^1]: Here you can find the text for the footnote. +[^2]: **Footnotes** themselves can also be *formatted*. +And these even include several lines. + + +------ asterisk ------ +This *is* an \*example with an asterisk\**. +This _is_ an \_example with an asterisk\_. + +------ bold ------ +test +**test** +__test__ + +------ italic ------ +test +*test* +_test_ + +------ italic + bold ------ +***Italic and Bold Text*** +___Italic and Bold Text___ + +------ stroke ------ +test +~test~ +~~test~~ diff --git a/addons/gdUnit4/test/update/resources/markdown_example.txt b/addons/gdUnit4/test/update/resources/markdown_example.txt new file mode 100644 index 0000000..54dd073 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/markdown_example.txt @@ -0,0 +1,22 @@ +## GdUnit3 v0.9.4 - Release Candidate + +### Improvements +- Added project settings to configure: + - Verbose Orphans to enable/disable report detected orphans +![image](https://user-images.githubusercontent.com/347037/119266895-e09d1900-bbec-11eb-91e9-45409ba2edb2.png) + - Server Connection Timeout Minites to set test server connection timeout in minutes + - Test Timeout Seconds to set the default test case timeout in seconds +![image](https://user-images.githubusercontent.com/347037/119266875-d1b66680-bbec-11eb-856f-8fac9b0ed31c.png) + +test seperator +--- + +### Bugfixes +- GdUnit inspecor: + - Fixed invalid test case state visualisation for detected orphan nodes (#63) + - Fixed a ui bug to auto select the first report failure after a test run + - Fixed invalid visualisation state and error counter (#66) +- TestSuite: + - Using asserts on stage after() now reporting +- Core: + - The GdUnit network layer was replaced by a new TCP server/client architecture to enable network-related testing (#64 ) diff --git a/addons/gdUnit4/test/update/resources/markdown_table.txt b/addons/gdUnit4/test/update/resources/markdown_table.txt new file mode 100644 index 0000000..6fbdad6 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/markdown_table.txt @@ -0,0 +1,6 @@ + type | description +-- | -- +any_color() | Argument matcher to match any Color value +any_vector2() | Argument matcher to match any Vector2 value +any_vector3() | Argument matcher to match any Vector3 value +any_rect2() | Argument matcher to match any Rect2 value diff --git a/addons/gdUnit4/test/update/resources/md_header.txt b/addons/gdUnit4/test/update/resources/md_header.txt new file mode 100644 index 0000000..b7669b0 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/md_header.txt @@ -0,0 +1,11 @@ +# Header 1 Text + +## Header 2 Text + +### Header 3 Text + +#### Header 4 Text + +##### Header 5 Text + +###### Header 6 Text diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd new file mode 100644 index 0000000..eedb5b2 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init() -> void: + super(GdUnit4Version.parse("v0.9.5")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd.uid b/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd.uid new file mode 100644 index 0000000..8502d2a --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd.uid @@ -0,0 +1 @@ +uid://tjojigt424sp diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd new file mode 100644 index 0000000..00e4e43 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init() -> void: + super(GdUnit4Version.parse("v0.9.6")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd.uid b/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd.uid new file mode 100644 index 0000000..db6bb58 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd.uid @@ -0,0 +1 @@ +uid://cg0xjvq0hgkv6 diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd new file mode 100644 index 0000000..867210f --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init() -> void: + super(GdUnit4Version.parse("v0.9.9-a")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd.uid b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd.uid new file mode 100644 index 0000000..cbfac3e --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd.uid @@ -0,0 +1 @@ +uid://bo5vopsyu8677 diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd new file mode 100644 index 0000000..a748c4b --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init() -> void: + super(GdUnit4Version.parse("v0.9.9-b")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd.uid b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd.uid new file mode 100644 index 0000000..b76974d --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd.uid @@ -0,0 +1 @@ +uid://geaxax2amvky diff --git a/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd b/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd new file mode 100644 index 0000000..aae0ee1 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init() -> void: + super(GdUnit4Version.parse("v1.1.4")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd.uid b/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd.uid new file mode 100644 index 0000000..29681d3 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd.uid @@ -0,0 +1 @@ +uid://difnuw2hlr171 diff --git a/addons/gdUnit4/test/update/resources/update.zip b/addons/gdUnit4/test/update/resources/update.zip new file mode 100644 index 0000000..f58a854 Binary files /dev/null and b/addons/gdUnit4/test/update/resources/update.zip differ diff --git a/assets/icons/icon.png.import b/assets/icons/icon.png.import index 32bd24a..1002830 100644 --- a/assets/icons/icon.png.import +++ b/assets/icons/icon.png.import @@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.png-816117353ba57afc3af521f78eedcc3c.cte compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -25,6 +27,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/assets/locales/ui_strings.csv.import b/assets/locales/ui_strings.csv.import index c81a590..f5c7247 100644 --- a/assets/locales/ui_strings.csv.import +++ b/assets/locales/ui_strings.csv.import @@ -15,3 +15,5 @@ dest_files=["res://assets/locales/ui_strings.en.translation"] compress=1 delimiter=0 +unescape_keys=false +unescape_translations=true diff --git a/docs/JULES_PROTOCOL.md b/docs/JULES_PROTOCOL.md index 68bb4b4..5945df0 100644 --- a/docs/JULES_PROTOCOL.md +++ b/docs/JULES_PROTOCOL.md @@ -29,7 +29,7 @@ Google Jules is an autonomous coding agent used to offload programming-heavy tas ## Post-Submission Handling 1. Once Jules submits a PR, the QA Lead is notified. -2. QA Lead runs the validation suite (`tests/run_all_tests.sh`). +2. QA Lead runs the validation suite (`tools/pre_push_check.sh` which executes GdUnit4). 3. If any automated test fails, the PR is closed immediately, and the `jules` label is removed from the issue for manual triage. 4. If all tests pass, QA Lead performs a manual code review and sign-off on the checklist. 5. RM performs a final check before merging. diff --git a/docs/JULES_QA_CHECKLIST.md b/docs/JULES_QA_CHECKLIST.md index 947c183..ff143f2 100644 --- a/docs/JULES_QA_CHECKLIST.md +++ b/docs/JULES_QA_CHECKLIST.md @@ -5,8 +5,7 @@ All PRs submitted by Google Jules must pass this checklist before being consider ## 1. Automated Testing - [ ] **Deterministic Math Validation**: Run `python3 tests/validate_math.py`. All tests must pass with 0 errors. -- [ ] **In-Engine Math Validation**: Run `godot --script tests/test_deterministic_math.gd --headless`. All engine-side math must match expected deterministic outputs. -- [ ] **Full Test Suite**: Run `bash tests/run_all_tests.sh`. All unit and integration tests must pass. +- [ ] **Full Test Suite**: Run `bash tools/pre_push_check.sh` (which executes GdUnit4 tests). All unit and integration tests must pass. ## 2. Integrity Checks diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 1cdec30..6681059 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -6,10 +6,10 @@ Use this checklist before every tag-driven release. No stage may be skipped. ## 1. QA Validation Gate -- [ ] All automated tests pass (`tests/run_all_tests.sh`). +- [ ] All automated tests pass (`tools/pre_push_check.sh`). - [ ] Python mirror validation passes (`python3 tests/validate_math.py`). - [ ] Palette validation passes if available (`python3 tests/validate_palettes.py`). -- [ ] In-engine test runner completes without failures. +- [ ] In-engine GdUnit4 test runner completes without failures. - [ ] **QA Lead sign-off required.** ## 2. Version Verification diff --git a/localization/ui_strings.csv.import b/localization/ui_strings.csv.import index ba3cc4a..fee7afc 100644 --- a/localization/ui_strings.csv.import +++ b/localization/ui_strings.csv.import @@ -15,3 +15,5 @@ dest_files=["res://localization/ui_strings.en.translation", "res://localization/ compress=1 delimiter=0 +unescape_keys=false +unescape_translations=true diff --git a/project.godot b/project.godot index f1ecdc5..0f36938 100644 --- a/project.godot +++ b/project.godot @@ -46,8 +46,8 @@ CaptionManager="*res://scripts/autoload/caption_manager.gd" AudioMiddleware="*res://scripts/autoload/audio_middleware.gd" BurdenCaptionDriver="*res://scripts/autoload/burden_caption_driver.gd" BurdenEventCoordinator="*res://scripts/autoload/burden_event_coordinator.gd" -EntityLifecycle="*res://scripts/entities/entity_lifecycle.gd" ## Exception: lives in entities/ (co-located with Entity data class); moving would break UIDs -RunManager="*res://scripts/state_machine/run_manager.gd" ## Exception: lives in state_machine/ (owns BaseStateMachine); moving would break UIDs +EntityLifecycle="*res://scripts/entities/entity_lifecycle.gd" +RunManager="*res://scripts/state_machine/run_manager.gd" GridSystem="*res://scripts/autoload/grid_system.gd" LayerManager="*res://scripts/autoload/layer_manager.gd" LocalizationManager="*res://scripts/autoload/localization_manager.gd" @@ -64,3 +64,7 @@ locale/fallback="en" [physics] 2d/run_on_separate_thread=true + +[editor_plugins] + +enabled=PackedStringArray("res://addons/gdUnit4/plugin.cfg") diff --git a/scripts/ai/simple_ai.gd.uid b/scripts/ai/simple_ai.gd.uid new file mode 100644 index 0000000..c326037 --- /dev/null +++ b/scripts/ai/simple_ai.gd.uid @@ -0,0 +1 @@ +uid://uoyhvy1vagff diff --git a/scripts/autoload/localization_manager.gd.uid b/scripts/autoload/localization_manager.gd.uid new file mode 100644 index 0000000..11c6727 --- /dev/null +++ b/scripts/autoload/localization_manager.gd.uid @@ -0,0 +1 @@ +uid://bfirle3em8kg6 diff --git a/scripts/core/astar_grid.gd b/scripts/core/astar_grid.gd index 0a957e5..41bfc08 100644 --- a/scripts/core/astar_grid.gd +++ b/scripts/core/astar_grid.gd @@ -6,8 +6,8 @@ extends RefCounted ## Performance target: โ‰ค2 ms per query on target hardware. ## Threading: main thread only. -const GRID_SIZE: int = GridSystem.GRID_SIZE -const TOTAL_TILES: int = GridSystem.TOTAL_TILES +const GRID_SIZE: int = _GridSystem.GRID_SIZE +const TOTAL_TILES: int = _GridSystem.TOTAL_TILES const COST_STRAIGHT: int = 10 ## Four positive-direction quadrants. connect_points(bidirectional=true) @@ -53,22 +53,27 @@ func _reset_search() -> void: ## (inclusive). Returns an empty array if no path exists. ## Threading: main thread ONLY. func find_path(start: Vector2i, goal: Vector2i) -> Array[Vector2i]: - if not GridSystem.is_in_bounds(start.x, start.y) or not GridSystem.is_in_bounds(goal.x, goal.y): + if ( + not Engine.get_main_loop().root.get_node("GridSystem").is_in_bounds(start.x, start.y) + or not Engine.get_main_loop().root.get_node("GridSystem").is_in_bounds(goal.x, goal.y) + ): return [] if start == goal: return [start] - var goal_i: int = GridSystem.index(goal.x, goal.y) - var goal_tile: TacTileData = GridSystem.get_tile_by_index(goal_i) + var goal_i: int = Engine.get_main_loop().root.get_node("GridSystem").index(goal.x, goal.y) + var goal_tile: TacTileData = ( + Engine.get_main_loop().root.get_node("GridSystem").get_tile_by_index(goal_i) + ) if goal_tile == null or goal_tile.blocks_movement: return [] ## Sync graph topology with GridSystem when the room has changed. - if _cached_room_id != GridSystem.room_id: + if _cached_room_id != Engine.get_main_loop().root.get_node("GridSystem").room_id: _rebuild_graph() - _cached_room_id = GridSystem.room_id + _cached_room_id = Engine.get_main_loop().root.get_node("GridSystem").room_id - var start_i: int = GridSystem.index(start.x, start.y) + var start_i: int = Engine.get_main_loop().root.get_node("GridSystem").index(start.x, start.y) var ids: PackedInt64Array = _astar.get_id_path(start_i, goal_i) if ids.is_empty(): return [] @@ -97,34 +102,50 @@ func _rebuild_graph() -> void: ## - diagonals require both intermediate cardinal cells to be passable for x: int in range(GRID_SIZE): for y: int in range(GRID_SIZE): - var i: int = GridSystem.index(x, y) - var tile: TacTileData = GridSystem.get_tile_by_index(i) + var i: int = Engine.get_main_loop().root.get_node("GridSystem").index(x, y) + var tile: TacTileData = ( + Engine.get_main_loop().root.get_node("GridSystem").get_tile_by_index(i) + ) if tile == null or tile.blocks_movement: continue for d: Vector2i in DIRS: var nx: int = x + d.x var ny: int = y + d.y - if not GridSystem.is_in_bounds(nx, ny): + if not Engine.get_main_loop().root.get_node("GridSystem").is_in_bounds(nx, ny): continue - var ni: int = GridSystem.index(nx, ny) - var ntile: TacTileData = GridSystem.get_tile_by_index(ni) + var ni: int = Engine.get_main_loop().root.get_node("GridSystem").index(nx, ny) + var ntile: TacTileData = ( + Engine.get_main_loop().root.get_node("GridSystem").get_tile_by_index(ni) + ) if ntile == null or ntile.blocks_movement: continue ## Prevent corner-cutting: diagonals require both adjacent ## cardinal cells to be passable from the start tile. - var forward_ok: bool = GridSystem.can_move(x, y, nx, ny) - var reverse_ok: bool = GridSystem.can_move(nx, ny, x, y) + var forward_ok: bool = Engine.get_main_loop().root.get_node("GridSystem").can_move( + x, y, nx, ny + ) + var reverse_ok: bool = Engine.get_main_loop().root.get_node("GridSystem").can_move( + nx, ny, x, y + ) if d.x != 0 and d.y != 0: if forward_ok: if ( - not GridSystem.can_move(x, y, x + d.x, y) - or not GridSystem.can_move(x, y, x, y + d.y) + not Engine.get_main_loop().root.get_node("GridSystem").can_move( + x, y, x + d.x, y + ) + or not Engine.get_main_loop().root.get_node("GridSystem").can_move( + x, y, x, y + d.y + ) ): forward_ok = false if reverse_ok: if ( - not GridSystem.can_move(nx, ny, x + d.x, y) - or not GridSystem.can_move(nx, ny, x, y + d.y) + not Engine.get_main_loop().root.get_node("GridSystem").can_move( + nx, ny, x + d.x, y + ) + or not Engine.get_main_loop().root.get_node("GridSystem").can_move( + nx, ny, x, y + d.y + ) ): reverse_ok = false diff --git a/scripts/entities/base_enemy.gd.uid b/scripts/entities/base_enemy.gd.uid new file mode 100644 index 0000000..db5c17d --- /dev/null +++ b/scripts/entities/base_enemy.gd.uid @@ -0,0 +1 @@ +uid://c764c4gau7v5a diff --git a/scripts/ui/modal.gd b/scripts/ui/modal.gd index b605d0d..475fb73 100644 --- a/scripts/ui/modal.gd +++ b/scripts/ui/modal.gd @@ -17,8 +17,12 @@ func _ready() -> void: func setup(title_key: String, body_key: String) -> void: - title_label.text = tr(title_key) - body_label.text = tr(body_key) + if not is_node_ready(): + await ready + if title_label: + title_label.text = tr(title_key) + if body_label: + body_label.text = tr(body_key) func dismiss() -> void: diff --git a/scripts/ui/toast_widget.gd b/scripts/ui/toast_widget.gd index c8c3ee8..30f4451 100644 --- a/scripts/ui/toast_widget.gd +++ b/scripts/ui/toast_widget.gd @@ -2,7 +2,7 @@ extends Control signal finished -@onready var panel: Panel = $Panel as Panel +@onready var panel: PanelContainer = $Panel as PanelContainer @onready var label: Label = $Panel/Label as Label diff --git a/tests/benchmark/benchmark_pathfinding.gd b/tests/benchmark/benchmark_pathfinding.gd index 394fb28..09da881 100644 --- a/tests/benchmark/benchmark_pathfinding.gd +++ b/tests/benchmark/benchmark_pathfinding.gd @@ -17,7 +17,9 @@ func _ready() -> void: var wall_tiles: Array[Dictionary] = [] for x: int in range(2, 10): wall_tiles.append({"x": x, "y": 6, "blocks_movement": true, "blocks_vision": true}) - GridSystem.load_room({"id": "bench_wall", "tiles": wall_tiles}) + Engine.get_main_loop().root.get_node("GridSystem").load_room( + {"id": "bench_wall", "tiles": wall_tiles} + ) var astar: AStarGrid = AStarGrid.new() var path: PackedVector2Array = astar.find_path(Vector2i(0, 0), Vector2i(11, 11)) diff --git a/tests/benchmark_gridsystem_script.gd b/tests/benchmark_gridsystem_script.gd index 85e29a2..d79a65b 100644 --- a/tests/benchmark_gridsystem_script.gd +++ b/tests/benchmark_gridsystem_script.gd @@ -5,13 +5,15 @@ func _ready() -> void: print("Warming up...") var t0: int = Time.get_ticks_usec() for i: int in range(10): - GridSystem._recompute_cover_cache() + Engine.get_main_loop().root.get_node("GridSystem")._recompute_cover_cache() var dt: int = Time.get_ticks_usec() - int(t0) print("Recompute cover cache x10 took: ", float(dt) / 1000.0, " ms") t0 = Time.get_ticks_usec() for i: int in range(100): - GridSystem.load_room({"id": "bench_room", "tiles": []}) + Engine.get_main_loop().root.get_node("GridSystem").load_room( + {"id": "bench_room", "tiles": []} + ) dt = Time.get_ticks_usec() - int(t0) print("Load room x100 took: ", float(dt) / 1000.0, " ms") get_tree().quit() diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh deleted file mode 100755 index 6811851..0000000 --- a/tests/run_all_tests.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -## Run all Emberfall test suites sequentially. -## Exit code 0 = all passed; non-zero = at least one failure. -## -## Invocation patterns used: -## 1. Python โ€” python3 tests/validate_math.py -## 2. SceneTree -s โ€” extends SceneTree, entry via _initialize() -## 3. Node -s โ€” extends Node, entry via _ready(); requires SceneTree host -## -## Excluded: -## test_ui_systems.gd โ€” intentional headless guard (if not OS.has_feature("headless")) -## produces no pass/fail output in CI. - -set -euo pipefail - -function cleanup() { - rm -f tests/temp_runner.gd -} -trap cleanup EXIT - -PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$PROJECT_DIR" - -GODOT_BIN="${GODOT_BIN:-godot}" - -echo "=== Emberfall Test Suite ===" - -# โ”€โ”€ 1. Python cross-platform math validator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -echo "" -echo "--- Running: validate_math.py (Python) ---" -python3 tests/validate_math.py - -# โ”€โ”€ 2. SceneTree-based tests (extends SceneTree, _initialize() entry) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -echo "" -echo "--- Running: test_deterministic_math.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_deterministic_math.gd - -echo "" -echo "--- Running: test_entity_lifecycle.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_entity_lifecycle.gd - -echo "" -echo "--- Running: test_elemental_resolver.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_elemental_resolver.gd - -echo "" -echo "--- Running: test_burden_stem_caption_router.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_burden_stem_caption_router.gd - -echo "" -echo "--- Running: test_run_determinism.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_run_determinism.gd - -echo "" -echo "--- Running: test_remap_ui.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_remap_ui.gd - -echo "" -echo "--- Running: test_localization_manager.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_localization_manager.gd - -# โ”€โ”€ 3. Node-based tests (extends Node, _ready() entry, quit() on done) โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# These need a SceneTree host. We use an inline bootstrap โ€” same mechanism -# as tests/test_runner.gd โ€” so no extra scene files are required. - -run_node_test() { - local SCRIPT_RES_PATH="$1" ## e.g. res://tests/test_state_machine.gd - local LABEL="$2" - echo "" - echo "--- Running: ${LABEL} ---" - cat < tests/temp_runner.gd -extends SceneTree -func _initialize() -> void: - var t: Node = (load("${SCRIPT_RES_PATH}") as GDScript).new() - root.add_child(t) -GDEOF - "$GODOT_BIN" --headless --path . -s tests/temp_runner.gd -} - -run_node_test "res://tests/test_state_machine.gd" "test_state_machine.gd" -run_node_test "res://tests/test_apparition.gd" "test_apparition.gd" -run_node_test "res://tests/test_audio_wiring.gd" "test_audio_wiring.gd" -run_node_test "res://tests/test_caption_system.gd" "test_caption_system.gd" -run_node_test "res://tests/test_burden_event.gd" "test_burden_event.gd" -run_node_test "res://tests/test_ui_reflow.gd" "test_ui_reflow.gd" -run_node_test "res://tests/test_tile_data.gd" "test_tile_data.gd" - -echo "" -echo "" -echo "--- Running: test_base_enemy.gd ---" -"$GODOT_BIN" --headless --path . -s tests/test_base_enemy.gd - -echo "" -echo "=== ALL TEST SUITES PASSED ===" diff --git a/tests/test_apparition.gd b/tests/test_apparition.gd index 192edaf..1d6488a 100644 --- a/tests/test_apparition.gd +++ b/tests/test_apparition.gd @@ -1,34 +1,22 @@ -extends Node -## Unit tests for DON-83 Apparition Composite Render Pipeline. -## Run via Godot Editor test runner or `godot --headless --script tests/test_apparition.gd`. +extends GdUnitTestSuite # โ”€โ”€ BurdenManager Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_burden_manager_record_kill() -> bool: - # Arrange +func test_burden_manager_record_kill() -> void: var bm: Node = BurdenManager bm.reset() - # Act bm.record_sentient_kill("enemy_wraith_01", "Wraith") bm.record_sentient_kill("enemy_shade_02", "Shade") - # Assert var queue: Array[BurdenManager.BurdenKillRecord] = bm.get_kill_queue() - if queue.size() != 2: - push_error("Expected queue size 2, got %d" % queue.size()) - return false - if queue[0].enemy_id != "enemy_wraith_01": - push_error("Expected first kill wraith, got %s" % queue[0].enemy_id) - return false - if queue[1].display_name != "Shade": - push_error("Expected second kill name Shade, got %s" % queue[1].display_name) - return false - return true - - -func test_burden_manager_cap_at_three() -> bool: + assert_that(queue.size()).is_equal(2) + assert_that(queue[0].enemy_id).is_equal("enemy_wraith_01") + assert_that(queue[1].display_name).is_equal("Shade") + + +func test_burden_manager_cap_at_three() -> void: var bm: Node = BurdenManager bm.reset() @@ -38,174 +26,118 @@ func test_burden_manager_cap_at_three() -> bool: bm.record_sentient_kill("d", "D") var ids: PackedStringArray = bm.get_last_enemy_ids() - if ids.size() != 3: - push_error("Expected capped size 3, got %d" % ids.size()) - return false - if ids[0] != "b" or ids[2] != "d": - push_error("Expected FIFO eviction: got %s" % ids) - return false - return true + assert_that(ids.size()).is_equal(3) + assert_that(ids[0]).is_equal("b") + assert_that(ids[2]).is_equal("d") -func test_burden_manager_moral_weight_toggle() -> bool: +func test_burden_manager_moral_weight_toggle() -> void: var bm: Node = BurdenManager bm.reset() bm.burden_active = false bm.update_moral_weight(2) - if bm.burden_active != false: - push_error("Expected burden inactive at flag 2") - return false + assert_that(bm.burden_active).is_equal(false) bm.update_moral_weight(3) - if bm.burden_active != true: - push_error("Expected burden active at threshold 3") - return false + assert_that(bm.burden_active).is_equal(true) bm.update_moral_weight(1) - if bm.burden_active != false: - push_error("Expected burden deactivated when flag drops") - return false - return true + assert_that(bm.burden_active).is_equal(false) # โ”€โ”€ ApparitionRenderer Layout Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_renderer_layout_constants() -> bool: - if ApparitionRenderer.VERTICAL_OFFSETS.size() != 3: - push_error("Expected 3 vertical offsets") - return false - if ApparitionRenderer.OPACITY_TIERS.size() != 3: - push_error("Expected 3 opacity tiers") - return false - if ApparitionRenderer.SCALE_TIERS.size() != 3: - push_error("Expected 3 scale tiers") - return false - if ApparitionRenderer.VERTICAL_OFFSETS[0] != 0 or ApparitionRenderer.VERTICAL_OFFSETS[2] != 16: - push_error("Vertical offsets mismatch") - return false - if not DeterministicMath.floori(ApparitionRenderer.OPACITY_TIERS[0] * 100.0) == 55: - push_error("Opacity tier 0 should be ~55%%") - return false - return true +func test_renderer_layout_constants() -> void: + assert_that(ApparitionRenderer.VERTICAL_OFFSETS.size()).is_equal(3) + assert_that(ApparitionRenderer.OPACITY_TIERS.size()).is_equal(3) + assert_that(ApparitionRenderer.SCALE_TIERS.size()).is_equal(3) + assert_that(ApparitionRenderer.VERTICAL_OFFSETS[0]).is_equal(0) + assert_that(ApparitionRenderer.VERTICAL_OFFSETS[2]).is_equal(16) + assert_that(DeterministicMath.floori(ApparitionRenderer.OPACITY_TIERS[0] * 100.0)).is_equal(55) # โ”€โ”€ ApparitionStateMachine Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_state_machine_manifest_to_idle() -> bool: - var renderer: ApparitionRenderer = ApparitionRenderer.new() +func test_state_machine_manifest_to_idle() -> void: + var renderer: ApparitionRenderer = auto_free(ApparitionRenderer.new()) add_child(renderer) var sm: ApparitionStateMachine = renderer.state_machine - if sm == null: - push_error("ApparitionRenderer did not create state_machine in _ready()") - return false + assert_that(sm).is_not_null() - if sm.current_state != ApparitionStateMachine.ApparitionState.INACTIVE: - push_error("Expected initial state INACTIVE") - return false + assert_that(sm.current_state).is_equal(ApparitionStateMachine.ApparitionState.INACTIVE) sm.cmd_manifest() - if sm.current_state != ApparitionStateMachine.ApparitionState.MANIFEST: - push_error("Expected MANIFEST after cmd_manifest") - return false + assert_that(sm.current_state).is_equal(ApparitionStateMachine.ApparitionState.MANIFEST) - # Fast-forward manifest timer for _i: int in range(60): sm.update(0.016) renderer._process(0.016) - if sm.current_state != ApparitionStateMachine.ApparitionState.IDLE: - push_error("Expected IDLE after manifest duration elapsed") - return false - - renderer.queue_free() - return true + assert_that(sm.current_state).is_equal(ApparitionStateMachine.ApparitionState.IDLE) -func test_state_machine_recoil_z_promotion() -> bool: - var keeper: Node2D = Node2D.new() +func test_state_machine_recoil_z_promotion() -> void: + var keeper: Node2D = auto_free(Node2D.new()) keeper.z_index = 10 add_child(keeper) - var renderer: ApparitionRenderer = ApparitionRenderer.new() + var renderer: ApparitionRenderer = auto_free(ApparitionRenderer.new()) renderer.owner_z_index_offset = -1 renderer.bind_owner(keeper) keeper.add_child(renderer) var sm: ApparitionStateMachine = renderer.state_machine - if sm == null: - push_error("ApparitionRenderer did not create state_machine in _ready()") - return false + assert_that(sm).is_not_null() sm.cmd_manifest() for _i: int in range(60): sm.update(0.016) renderer._process(0.016) - # Simulate recoil sm.cmd_recoil() renderer._process(0.016) - if not sm._recoil_promotion_active: - push_error("Expected recoil promotion active flag") - return false - if renderer.z_index != 12: - push_error("Expected z_index promoted to %d, got %d" % [12, renderer.z_index]) - return false - - # Fast-forward recoil + assert_that(sm._recoil_promotion_active).is_true() + assert_that(renderer.z_index).is_equal(12) + for _i: int in range(10): sm.update(0.016) renderer._process(0.016) - # After recoil, z_index should restore - if renderer.z_index != 9: - push_error("Expected z_index restored to 9, got %d" % renderer.z_index) - return false - - keeper.queue_free() - return true + assert_that(renderer.z_index).is_equal(9) # โ”€โ”€ Keeper Integration Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_keeper_integration_damage_triggers_recoil() -> bool: +func test_keeper_integration_damage_triggers_recoil() -> void: BurdenManager.reset() - var k: Keeper = Keeper.new() + var k: Keeper = auto_free(Keeper.new()) add_child(k) - # Force manifestation via BurdenManager so the apparition is visible. BurdenManager.record_sentient_kill("enemy_test", "Test Enemy") BurdenManager.record_sentient_kill("enemy_test2", "Test Enemy 2") BurdenManager.record_sentient_kill("enemy_test3", "Test Enemy 3") BurdenManager.update_moral_weight(3) - # Fast-forward manifest for _i: int in range(60): k._process(0.016) if k._apparition: k._apparition._process(0.016) var sm: ApparitionStateMachine = k._apparition.state_machine - if sm.current_state != ApparitionStateMachine.ApparitionState.IDLE: - push_error("Expected IDLE after manifest") - return false + assert_that(sm.current_state).is_equal(ApparitionStateMachine.ApparitionState.IDLE) k.apply_damage(5) - if sm.current_state != ApparitionStateMachine.ApparitionState.RECOIL: - push_error("Expected RECOIL after apply_damage") - return false - - k.queue_free() - return true + assert_that(sm.current_state).is_equal(ApparitionStateMachine.ApparitionState.RECOIL) -func test_keeper_integration_kill_updates_stack() -> bool: +func test_keeper_integration_kill_updates_stack() -> void: BurdenManager.reset() - var k: Keeper = Keeper.new() + var k: Keeper = auto_free(Keeper.new()) add_child(k) k.record_sentient_kill("wolf_01", "Wolf") @@ -213,45 +145,4 @@ func test_keeper_integration_kill_updates_stack() -> bool: k.record_sentient_kill("wolf_03", "Wolf") var ids: PackedStringArray = BurdenManager.get_last_enemy_ids() - if ids.size() != 3: - push_error("Expected 3 kills recorded via Keeper") - return false - - k.queue_free() - return true - - -# โ”€โ”€ Test Runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - -func _ready() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_burden_manager_record_kill", - "test_burden_manager_cap_at_three", - "test_burden_manager_moral_weight_toggle", - "test_renderer_layout_constants", - "test_state_machine_manifest_to_idle", - "test_state_machine_recoil_z_promotion", - "test_keeper_integration_damage_triggers_recoil", - "test_keeper_integration_kill_updates_stack", - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL (returned %s)" % str(ok)) - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - push_error("Apparition test suite had failures.") - get_tree().quit(1) - else: - get_tree().quit(0) + assert_that(ids.size()).is_equal(3) diff --git a/tests/test_astar_grid.gd b/tests/test_astar_grid.gd index 31e445f..75d276e 100644 --- a/tests/test_astar_grid.gd +++ b/tests/test_astar_grid.gd @@ -1,113 +1,87 @@ -extends Node2D -## Smoke test for AStarGrid +extends GdUnitTestSuite const AStarGrid := preload("res://scripts/core/astar_grid.gd") -func _ready() -> void: - var ok := true - - ## 1. Straight line on empty grid - GridSystem.load_room({"id": "empty", "tiles": []}) +func test_straight_line_on_empty_grid() -> void: + Engine.get_main_loop().root.get_node("GridSystem").load_room({"id": "empty", "tiles": []}) var astar: AStarGrid = AStarGrid.new() var path: PackedVector2Array = astar.find_path(Vector2i(0, 0), Vector2i(11, 11)) - if path.is_empty(): - print("FAIL: empty grid path not found") - ok = false - elif path[0] != Vector2(0, 0) or path[path.size() - 1] != Vector2(11, 11): - print("FAIL: wrong endpoints on empty grid") - ok = false - else: - print("PASS: straight line") - - ## 2. Wall detour + + assert_that(path.is_empty()).is_false() + assert_that(path[0]).is_equal(Vector2(0, 0)) + assert_that(path[path.size() - 1]).is_equal(Vector2(11, 11)) + + +func test_wall_detour() -> void: var wall_tiles: Array[Dictionary] = [] for x: int in range(2, 10): wall_tiles.append({"x": x, "y": 6, "blocks_movement": true, "blocks_vision": true}) - GridSystem.load_room({"id": "wall", "tiles": wall_tiles}) - astar = AStarGrid.new() - path = astar.find_path(Vector2i(0, 0), Vector2i(11, 11)) - if path.is_empty(): - print("FAIL: wall detour path not found") - ok = false - else: - for i: int in range(path.size()): - var tile := GridSystem.get_tile(int(path[i].x), int(path[i].y)) - if tile != null and tile.is_blocked(): - print("FAIL: path crosses wall at ", path[i]) - ok = false - break - if ok: - print("PASS: wall detour") - - ## 3. Elevation blocking โ€” path must not step across ฮ”elevation > 1 + Engine.get_main_loop().root.get_node("GridSystem").load_room( + {"id": "wall", "tiles": wall_tiles} + ) + var astar: AStarGrid = AStarGrid.new() + var path: PackedVector2Array = astar.find_path(Vector2i(0, 0), Vector2i(11, 11)) + + assert_that(path.is_empty()).is_false() + for i: int in range(path.size()): + var tile: TacTileData = Engine.get_main_loop().root.get_node("GridSystem").get_tile( + int(path[i].x), int(path[i].y) + ) + if tile != null: + assert_that(tile.is_blocked()).is_false() + + +func test_elevation_blocking() -> void: var elev_tiles: Array[Dictionary] = [ {"x": 1, "y": 0, "elevation": 2}, ] - GridSystem.load_room({"id": "elev", "tiles": elev_tiles}) - astar = AStarGrid.new() - path = astar.find_path(Vector2i(0, 0), Vector2i(2, 0)) - if path.is_empty(): - ## Goal may be unreachable if fully boxed; on open grid a detour exists. - print("PASS: elevation blocking (no path)") - else: - var step_ok := true + Engine.get_main_loop().root.get_node("GridSystem").load_room( + {"id": "elev", "tiles": elev_tiles} + ) + var astar: AStarGrid = AStarGrid.new() + var path: PackedVector2Array = astar.find_path(Vector2i(0, 0), Vector2i(2, 0)) + + if not path.is_empty(): for i: int in range(path.size() - 1): - var a := path[i] - var b := path[i + 1] - var ta := GridSystem.get_tile(int(a.x), int(a.y)) - var tb := GridSystem.get_tile(int(b.x), int(b.y)) - if ta == null or tb == null: - step_ok = false - break - if abs(ta.elevation - tb.elevation) > 1: - step_ok = false - break - if step_ok: - print("PASS: elevation blocking (detour valid)") - else: - print("FAIL: elevation blocking (illegal step)") - ok = false - - ## 4. Completely boxed goal + var a: Vector2 = path[i] + var b: Vector2 = path[i + 1] + var ta: TacTileData = Engine.get_main_loop().root.get_node("GridSystem").get_tile( + int(a.x), int(a.y) + ) + var tb: TacTileData = Engine.get_main_loop().root.get_node("GridSystem").get_tile( + int(b.x), int(b.y) + ) + assert_that(ta).is_not_null() + assert_that(tb).is_not_null() + assert_that(abs(ta.elevation - tb.elevation) <= 1).is_true() + + +func test_completely_boxed_goal() -> void: var box_tiles: Array[Dictionary] = [ {"x": 1, "y": 0, "blocks_movement": true}, {"x": 0, "y": 1, "blocks_movement": true}, {"x": 1, "y": 1, "blocks_movement": true}, ] - GridSystem.load_room({"id": "box", "tiles": box_tiles}) - astar = AStarGrid.new() - path = astar.find_path(Vector2i(0, 0), Vector2i(0, 0)) - if path.size() != 1 or path[0] != Vector2(0, 0): - print("FAIL: start==goal should return single-point path") - ok = false - else: - print("PASS: start==goal") + Engine.get_main_loop().root.get_node("GridSystem").load_room({"id": "box", "tiles": box_tiles}) + var astar: AStarGrid = AStarGrid.new() + + var path: PackedVector2Array = astar.find_path(Vector2i(0, 0), Vector2i(0, 0)) + assert_that(path.size()).is_equal(1) + assert_that(path[0]).is_equal(Vector2(0, 0)) path = astar.find_path(Vector2i(0, 0), Vector2i(1, 1)) - if not path.is_empty(): - print("FAIL: boxed goal should be unreachable") - ok = false - else: - print("PASS: boxed goal unreachable") + assert_that(path.is_empty()).is_true() + - ## 5. Corner-cutting prevention +func test_corner_cutting_prevention() -> void: var corner_tiles: Array[Dictionary] = [ {"x": 1, "y": 0, "blocks_movement": true}, {"x": 0, "y": 1, "blocks_movement": true}, ] - GridSystem.load_room({"id": "corner", "tiles": corner_tiles}) - astar = AStarGrid.new() - path = astar.find_path(Vector2i(0, 0), Vector2i(1, 1)) - if not path.is_empty(): - print("FAIL: diagonal corner-cut should be blocked") - ok = false - else: - print("PASS: corner-cutting prevention") - - if ok: - print("\nALL TESTS PASSED") - get_tree().quit(0) - else: - print("\nSOME TESTS FAILED") - get_tree().quit(1) + Engine.get_main_loop().root.get_node("GridSystem").load_room( + {"id": "corner", "tiles": corner_tiles} + ) + var astar: AStarGrid = AStarGrid.new() + var path: PackedVector2Array = astar.find_path(Vector2i(0, 0), Vector2i(1, 1)) + assert_that(path.is_empty()).is_true() diff --git a/tests/test_audio_wiring.gd b/tests/test_audio_wiring.gd index 116a7af..7bc61b4 100644 --- a/tests/test_audio_wiring.gd +++ b/tests/test_audio_wiring.gd @@ -1,42 +1,11 @@ -extends Node - -## Integration tests for Audio Stem Event Wiring (DON-218). - - -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_stem_playback_signals", - "test_audio_middleware_forwarding", - "test_caption_driver_mapping", - "test_caption_driver_cooldown" - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL (returned %s)" % str(ok)) - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - push_error("Audio Wiring test suite had failures.") - get_tree().quit(1) - else: - get_tree().quit(0) - - -func test_stem_playback_signals() -> bool: - var playback: _StemPlayback = _StemPlayback.new("BD-BASS", "Master") +extends GdUnitTestSuite + + +func test_stem_playback_signals() -> void: + var playback: _StemPlayback = auto_free(_StemPlayback.new("BD-BASS", "Master")) add_child(playback) - var results := {"signal_emitted": false, "emitted_intensity": 0.0} + var results: Dictionary = {"signal_emitted": false, "emitted_intensity": 0.0} playback.transient_detected.connect( func(type: String, intensity: float) -> void: results.signal_emitted = true @@ -46,24 +15,17 @@ func test_stem_playback_signals() -> bool: # Manually trigger internal analysis result (mocking spectrum analyzer impact) playback.transient_detected.emit("impact", 0.8) - if not results.signal_emitted: - push_error("Expected transient_detected signal to be emitted") - return false - if results.emitted_intensity != 0.8: - push_error("Expected intensity 0.8, got %f" % results.emitted_intensity) - return false - - playback.queue_free() - return true + assert_that(results.signal_emitted).is_true() + assert_that(results.emitted_intensity).is_equal(0.8) -func test_audio_middleware_forwarding() -> bool: - var am: _AudioMiddleware = _AudioMiddleware.new() +func test_audio_middleware_forwarding() -> void: + var am: _AudioMiddleware = auto_free(_AudioMiddleware.new()) add_child(am) # Need to call _ready manually if not in tree at start am._ready() - var results := {"signal_emitted": false} + var results: Dictionary = {"signal_emitted": false} am.stem_event_detected.connect( func(stem_id: String, type: String, _intensity: float) -> void: if stem_id == "BD-MECH" and type == "clang": @@ -74,34 +36,27 @@ func test_audio_middleware_forwarding() -> bool: var mech_playback: _StemPlayback = am.get_node("BD_MECH") as _StemPlayback mech_playback.transient_detected.emit("clang", 0.5) - if not results.signal_emitted: - push_error("AudioMiddleware failed to forward stem signal") - return false + assert_that(results.signal_emitted).is_true() - am.queue_free() - return true - -func test_caption_driver_mapping() -> bool: - # Mock CaptionManager - var cm: Node = Node.new() +func test_caption_driver_mapping() -> void: + var cm: Node = auto_free(Node.new()) cm.name = "CaptionManager" - # Replace existing autoload node if it exists var existing_cm: Node = get_tree().root.get_node_or_null("CaptionManager") if existing_cm: get_tree().root.remove_child(existing_cm) get_tree().root.add_child(cm) - var results := {"caption_received": false, "received_text": ""} + var results: Dictionary = {"caption_received": false, "received_text": ""} - # Add dummy schedule method var script: GDScript = GDScript.new() script.source_code = ( "extends Node\n" + "signal scheduled(text: String)\n" - + "func schedule(text: String, _channel: int, _offset: float, _duration: float, _curve: int, _loc_key: String) -> void:\n" + + "func schedule(text: String, _channel: int, _offset: float, " + + "_duration: float, _curve: int, _loc_key: String) -> void:\n" + " scheduled.emit(text)\n" + "func report_stem_transient(_a: String, _b: String, _c: float) -> void: pass" ) @@ -114,42 +69,24 @@ func test_caption_driver_mapping() -> bool: results.received_text = text ) - var driver: _BurdenCaptionDriver = _BurdenCaptionDriver.new() + var driver: _BurdenCaptionDriver = auto_free(_BurdenCaptionDriver.new()) add_child(driver) # Manually trigger event that should map to a caption driver._on_stem_event("BD-BASS", "impact", 0.9) - if not results.caption_received: - push_error("BurdenCaptionDriver failed to trigger caption") - cm.queue_free() - if existing_cm: - get_tree().root.add_child(existing_cm) - return false - - if results.received_text != "[Deep impact]": - push_error("Expected '[Deep impact]', got '%s'" % results.received_text) - cm.queue_free() - if existing_cm: - get_tree().root.add_child(existing_cm) - return false - - driver.queue_free() - cm.queue_free() + assert_that(results.caption_received).is_true() + assert_that(results.received_text).is_equal("[Deep impact]") - # Restore existing autoload + get_tree().root.remove_child(cm) if existing_cm: get_tree().root.add_child(existing_cm) - return true - -func test_caption_driver_cooldown() -> bool: - # Mock CaptionManager - var cm: Node = Node.new() +func test_caption_driver_cooldown() -> void: + var cm: Node = auto_free(Node.new()) cm.name = "CaptionManager" - # Replace existing autoload node if it exists var existing_cm: Node = get_tree().root.get_node_or_null("CaptionManager") if existing_cm: get_tree().root.remove_child(existing_cm) @@ -167,34 +104,18 @@ func test_caption_driver_cooldown() -> bool: script.reload() cm.set_script(script) - var results := {"call_count": 0} + var results: Dictionary = {"call_count": 0} cm.connect("scheduled", func() -> void: results.call_count += 1) - var driver: _BurdenCaptionDriver = _BurdenCaptionDriver.new() + var driver: _BurdenCaptionDriver = auto_free(_BurdenCaptionDriver.new()) add_child(driver) # Trigger same event twice rapidly driver._on_stem_event("BD-MECH", "clang", 0.9) driver._on_stem_event("BD-MECH", "clang", 0.9) - if results.call_count != 1: - push_error("Cooldown failed: expected 1 call, got %d" % results.call_count) - cm.queue_free() - if existing_cm: - get_tree().root.add_child(existing_cm) - return false - - driver.queue_free() - cm.queue_free() + assert_that(results.call_count).is_equal(1) - # Restore existing autoload + get_tree().root.remove_child(cm) if existing_cm: get_tree().root.add_child(existing_cm) - - return true - - -func _ready() -> void: - # Small delay to ensure all nodes are ready - await get_tree().process_frame - run_all() diff --git a/tests/test_base_enemy.gd b/tests/test_base_enemy.gd index 24ee929..cc4da0c 100644 --- a/tests/test_base_enemy.gd +++ b/tests/test_base_enemy.gd @@ -1,112 +1,47 @@ -extends SceneTree +extends GdUnitTestSuite -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_alive_returns_false_when_entity_null", - "test_alive_returns_true_when_entity_idle", - "test_alive_returns_true_when_entity_stunned", - "test_alive_returns_true_when_entity_dying", - "test_alive_returns_false_when_entity_dead", - "test_alive_returns_false_when_entity_ghost" - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL (returned %s)" % str(ok)) - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - push_error("BaseEnemy test suite had failures.") - quit(1) - else: - quit(0) - - -func test_alive_returns_false_when_entity_null() -> bool: - var enemy_scene: BaseEnemy = BaseEnemy.new() +func test_alive_returns_false_when_entity_null() -> void: + var enemy_scene: BaseEnemy = auto_free(BaseEnemy.new()) enemy_scene.entity = null - - var success: bool = enemy_scene.alive() == false - if not success: - push_error("Expected alive() to be false when entity is null") - enemy_scene.free() - return success + assert_that(enemy_scene.alive()).is_false() -func test_alive_returns_true_when_entity_idle() -> bool: - var enemy_scene: BaseEnemy = BaseEnemy.new() +func test_alive_returns_true_when_entity_idle() -> void: + var enemy_scene: BaseEnemy = auto_free(BaseEnemy.new()) var ent: Entity = Entity.new("Test", 0, 0, 10, 5, 3) ent.state = Entity.State.IDLE enemy_scene.entity = ent - - var success: bool = enemy_scene.alive() == true - if not success: - push_error("Expected alive() to be true when entity is IDLE") - enemy_scene.free() - return success + assert_that(enemy_scene.alive()).is_true() -func test_alive_returns_true_when_entity_stunned() -> bool: - var enemy_scene: BaseEnemy = BaseEnemy.new() +func test_alive_returns_true_when_entity_stunned() -> void: + var enemy_scene: BaseEnemy = auto_free(BaseEnemy.new()) var ent: Entity = Entity.new("Test", 0, 0, 10, 5, 3) ent.state = Entity.State.STUNNED enemy_scene.entity = ent + assert_that(enemy_scene.alive()).is_true() - var success: bool = enemy_scene.alive() == true - if not success: - push_error("Expected alive() to be true when entity is STUNNED") - enemy_scene.free() - return success - -func test_alive_returns_true_when_entity_dying() -> bool: - var enemy_scene: BaseEnemy = BaseEnemy.new() +func test_alive_returns_true_when_entity_dying() -> void: + var enemy_scene: BaseEnemy = auto_free(BaseEnemy.new()) var ent: Entity = Entity.new("Test", 0, 0, 10, 5, 3) ent.state = Entity.State.DYING enemy_scene.entity = ent - - var success: bool = enemy_scene.alive() == true - if not success: - push_error("Expected alive() to be true when entity is DYING") - enemy_scene.free() - return success + assert_that(enemy_scene.alive()).is_true() -func test_alive_returns_false_when_entity_dead() -> bool: - var enemy_scene: BaseEnemy = BaseEnemy.new() +func test_alive_returns_false_when_entity_dead() -> void: + var enemy_scene: BaseEnemy = auto_free(BaseEnemy.new()) var ent: Entity = Entity.new("Test", 0, 0, 10, 5, 3) ent.state = Entity.State.DEAD enemy_scene.entity = ent - - var success: bool = enemy_scene.alive() == false - if not success: - push_error("Expected alive() to be false when entity is DEAD") - enemy_scene.free() - return success + assert_that(enemy_scene.alive()).is_false() -func test_alive_returns_false_when_entity_ghost() -> bool: - var enemy_scene: BaseEnemy = BaseEnemy.new() +func test_alive_returns_false_when_entity_ghost() -> void: + var enemy_scene: BaseEnemy = auto_free(BaseEnemy.new()) var ent: Entity = Entity.new("Test", 0, 0, 10, 5, 3) ent.state = Entity.State.GHOST enemy_scene.entity = ent - - var success: bool = enemy_scene.alive() == false - if not success: - push_error("Expected alive() to be false when entity is GHOST") - enemy_scene.free() - return success - - -func _initialize() -> void: - run_all() + assert_that(enemy_scene.alive()).is_false() diff --git a/tests/test_base_enemy.gd.uid b/tests/test_base_enemy.gd.uid new file mode 100644 index 0000000..eba892b --- /dev/null +++ b/tests/test_base_enemy.gd.uid @@ -0,0 +1 @@ +uid://bby21as6wq4xy diff --git a/tests/test_burden_event.gd b/tests/test_burden_event.gd index 245d405..a4410a6 100644 --- a/tests/test_burden_event.gd +++ b/tests/test_burden_event.gd @@ -1,75 +1,21 @@ -extends Node -## Unit / integration tests for Gate 2-2 Burden Event systems. -## Run via Godot Editor test runner or `godot --headless --script tests/test_burden_event.gd`. -## -## Covers: -## AC-1: JSON schema config loads and validates -## AC-2: Save schema load/store round-trip -## AC-3: Numbness cap triggers at exactly N=5 with silent Phase B -## AC-4: Noun rotation is deterministic per seed and persists across runs -## AC-5: Localization keys are unique in the master table - - -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_config_loads", - "test_save_roundtrip", - "test_numbness_cap_exactly_five", - "test_noun_rotation_deterministic", - "test_variant_selection_fallback", - "test_localization_keys_unique", - "test_phase_b_timing_window", - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL (returned %s)" % str(ok)) - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - push_error("Burden Event test suite had failures.") - get_tree().quit(1) - else: - get_tree().quit(0) +extends GdUnitTestSuite # โ”€โ”€ AC-1: Config loads and schema validates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_config_loads() -> bool: +func test_config_loads() -> void: var bm: Node = BurdenManager # _ready() will attempt to load config automatically bm._ready() - if not bm._config_loaded: - push_error("Expected config to be loaded") - return false - if bm._event_engine._collective_nouns.size() != 8: - push_error( - "Expected 8 collective nouns, got %d" % bm._event_engine._collective_nouns.size() - ) - return false - if bm._event_engine._numbness_cap != 5: - push_error("Expected numbness_cap = 5, got %d" % bm._event_engine._numbness_cap) - return false - if bm._event_engine._variants_first.size() != 3: - push_error("Expected 3 first variants, got %d" % bm._event_engine._variants_first.size()) - return false - if bm._event_engine._variants_repeat.size() != 2: - push_error("Expected 2 repeat variants, got %d" % bm._event_engine._variants_repeat.size()) - return false - return true + assert_that(bm._config_loaded).is_true() + assert_that(bm._event_engine._collective_nouns.size()).is_equal(8) + assert_that(bm._event_engine._numbness_cap).is_equal(5) + assert_that(bm._event_engine._variants_first.size()).is_equal(3) + assert_that(bm._event_engine._variants_repeat.size()).is_equal(2) # โ”€โ”€ AC-2: Save schema round-trip โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_save_roundtrip() -> bool: +func test_save_roundtrip() -> void: var bm: Node = BurdenManager bm._ready() bm.reset() @@ -87,46 +33,27 @@ func test_save_roundtrip() -> bool: } ) ) - if bm._burden_noun_index != 3: - push_error("Expected noun_index 3 after load, got %d" % bm._burden_noun_index) - return false - if bm._lifetime_trigger_count != 7: - push_error( - "Expected lifetime_trigger_count 7 after load, got %d" % bm._lifetime_trigger_count - ) - return false + assert_that(bm._burden_noun_index).is_equal(3) + assert_that(bm._lifetime_trigger_count).is_equal(7) ## Run a new run, trigger once, then save bm.reset() bm.trigger_burden_event(12345, 67890, 0, 0, true) var saved: Dictionary = bm.save_memory_state() - if saved["burden_noun_index"] != bm._burden_noun_index: - push_error("Save noun_index mismatch") - return false - if saved["burden_trigger_history"] != 8: - push_error( - ( - "Expected lifetime_triggers 8 after one trigger, got %d" - % saved["burden_trigger_history"] - ) - ) - return false + + assert_that(saved["burden_noun_index"]).is_equal(bm._burden_noun_index) + assert_that(saved["burden_trigger_history"]).is_equal(8) ## Reset and reload must preserve cross-run values var noun_before: int = bm._burden_noun_index var lifetime_before: int = bm._lifetime_trigger_count bm.reset() - if bm._burden_noun_index != noun_before: - push_error("Reset must NOT clear persisted noun_index") - return false - if bm._lifetime_trigger_count != lifetime_before: - push_error("Reset must NOT clear lifetime_trigger_count") - return false - return true + assert_that(bm._burden_noun_index).is_equal(noun_before) + assert_that(bm._lifetime_trigger_count).is_equal(lifetime_before) # โ”€โ”€ AC-3: Numbness cap triggers at exactly N=5 with silent Phase B โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_numbness_cap_exactly_five() -> bool: +func test_numbness_cap_exactly_five() -> void: var bm: Node = BurdenManager bm._ready() bm.reset() @@ -140,28 +67,17 @@ func test_numbness_cap_exactly_five() -> bool: run_seed, topo_seed, room_index, i, i == 1 ) if i < 5: - if result.numbness_cap_reached: - push_error("Trigger #%d should NOT be numb" % i) - return false - if result.phase_b_text.is_empty(): - push_error("Trigger #%d Phase B text must NOT be empty" % i) - return false + assert_that(result.numbness_cap_reached).is_false() + assert_that(result.phase_b_text.is_empty()).is_false() else: # i == 5 and i == 6 should both be numb - if not result.numbness_cap_reached: - push_error("Trigger #%d MUST be numb" % i) - return false - if not result.phase_b_text.is_empty(): - push_error("Trigger #%d Phase B text MUST be empty (silent)" % i) - return false - if result.phase_b_localization_key != "BE_NUMBNESS_CAP": - push_error("Trigger #%d numbness localization key mismatch" % i) - return false - return true + assert_that(result.numbness_cap_reached).is_true() + assert_that(result.phase_b_text.is_empty()).is_true() + assert_that(result.phase_b_localization_key).is_equal("BE_NUMBNESS_CAP") # โ”€โ”€ AC-4: Noun rotation deterministic per seed and persists โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_noun_rotation_deterministic() -> bool: +func test_noun_rotation_deterministic() -> void: var bm1: Node = BurdenManager bm1._ready() bm1.reset() @@ -171,56 +87,40 @@ func test_noun_rotation_deterministic() -> bool: var noun1: String = bm1.select_collective_noun(topo, 0) var noun2: String = bm1.select_collective_noun(topo, 0) - if noun1 != noun2: - push_error("Noun selection must be deterministic for same seed") - return false + assert_that(noun1).is_equal(noun2) ## Different topology seed โ†’ potentially different noun var bm2: Node = BurdenManager bm2._ready() bm2.reset() - var _noun3: String = bm2.select_collective_noun(topo + 1, 0) - ## We only assert determinism, not that different seeds ALWAYS differ. - ## But we do assert the index is in range. - if bm1._burden_noun_index < 0 or bm1._burden_noun_index >= bm1._event_engine._noun_pool_size: - push_error("Noun index out of range: %d" % bm1._burden_noun_index) - return false + var noun3: String = bm2.select_collective_noun(topo + 1, 0) + + assert_that(bm1._burden_noun_index >= 0).is_true() + assert_that(bm1._burden_noun_index < bm1._event_engine._noun_pool_size).is_true() ## Persistence across runs bm1.reset() - if bm1._burden_noun_index != bm1._burden_noun_index: - # trivial, but we already tested this in save_roundtrip - pass - return true + assert_that(bm1._burden_noun_index).is_equal(bm1._burden_noun_index) # โ”€โ”€ Variant selection fallback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_variant_selection_fallback() -> bool: +func test_variant_selection_fallback() -> void: var bm: Node = BurdenManager bm._ready() bm.reset() var v: Dictionary = bm.select_variant_first(111, 0, 0) - if v.is_empty(): - push_error("Variant selection must not return empty") - return false - if not v.has("id"): - push_error("Variant must have 'id' field") - return false - if not v.has("localization_key"): - push_error("Variant must have 'localization_key' field") - return false + assert_that(v.is_empty()).is_false() + assert_that(v.has("id")).is_true() + assert_that(v.has("localization_key")).is_true() ## Repeat pool var vr: Dictionary = bm.select_variant_repeat(222, 1, 1) - if vr.is_empty(): - push_error("Repeat variant selection must not return empty") - return false - return true + assert_that(vr.is_empty()).is_false() # โ”€โ”€ AC-5: Localization keys unique โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_localization_keys_unique() -> bool: +func test_localization_keys_unique() -> void: var bm: Node = BurdenManager bm._ready() bm.reset() @@ -240,38 +140,19 @@ func test_localization_keys_unique() -> bool: for k: String in keys: if k.is_empty(): continue - if seen.has(k): - push_error("Duplicate localization key detected: %s" % k) - return false + assert_that(seen.has(k)).is_false() seen[k] = true - return true # โ”€โ”€ Phase B timing window verification โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_phase_b_timing_window() -> bool: +func test_phase_b_timing_window() -> void: var bm: Node = BurdenManager bm._ready() bm.reset() ## The JSON specifies min=10000, max=40000. - if not bm.is_within_phase_b_window(10000): - push_error("10 000 ms must be inside window") - return false - if not bm.is_within_phase_b_window(25000): - push_error("25 000 ms must be inside window") - return false - if not bm.is_within_phase_b_window(40000): - push_error("40 000 ms must be inside window") - return false - if bm.is_within_phase_b_window(5000): - push_error("5 000 ms must be outside window") - return false - if bm.is_within_phase_b_window(45000): - push_error("45 000 ms must be outside window") - return false - return true - - -# โ”€โ”€ Test Runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _ready() -> void: - run_all() + assert_that(bm.is_within_phase_b_window(10000)).is_true() + assert_that(bm.is_within_phase_b_window(25000)).is_true() + assert_that(bm.is_within_phase_b_window(40000)).is_true() + assert_that(bm.is_within_phase_b_window(5000)).is_false() + assert_that(bm.is_within_phase_b_window(45000)).is_false() diff --git a/tests/test_burden_stem_caption_router.gd b/tests/test_burden_stem_caption_router.gd index 6fcda3f..61c6b5a 100644 --- a/tests/test_burden_stem_caption_router.gd +++ b/tests/test_burden_stem_caption_router.gd @@ -1,91 +1,64 @@ -extends SceneTree - -## BurdenStemCaptionRouterValidation -## 9 cases for DON-223. +extends GdUnitTestSuite const RouterScript: GDScript = preload("res://scripts/core/burden_stem_caption_router.gd") +var _bm: Node + + +func before_all() -> void: + var root: Window = get_tree().root + if not root.has_node("BurdenManager"): + var bm_script: GDScript = load("res://scripts/autoload/burden_manager.gd") as GDScript + _bm = bm_script.new() + _bm.name = "BurdenManager" + root.add_child(_bm) + + +func after_all() -> void: + if _bm and is_instance_valid(_bm): + _bm.queue_free() + -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_config_loading", - "test_would_dispatch_true", - "test_would_dispatch_cooldown", - "test_would_dispatch_mwt_binding", - "test_dispatch_event_triggers_presenter", - "test_dispatch_event_applies_cooldown", - "test_reset_cooldowns", - "test_logical_event_volume_agnostic", - "test_climb_feature_mapping" - ] - - print("--- Running BurdenStemCaptionRouterValidation ---") - for name: String in tests: - print("Running %s..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL") - - print("\nResults: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - quit(1) - else: - quit(0) - - -func test_config_loading() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) - router.call("_ready") # Force load config +func test_config_loading() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) + router.call("_ready") var configs: Dictionary = router.get("_configs") as Dictionary - if configs.size() == 4: - router.queue_free() - return true - router.queue_free() - return false + assert_that(configs.size()).is_equal(4) -func test_would_dispatch_true() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) +func test_would_dispatch_true() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 3) var result: bool = bool(router.call("would_dispatch", "BD-BASS", "impact")) - router.queue_free() - return result + assert_that(result).is_true() -func test_would_dispatch_cooldown() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) +func test_would_dispatch_cooldown() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 3) router.call("dispatch_event", "BD-BASS", "impact") var result: bool = bool(router.call("would_dispatch", "BD-BASS", "impact")) - router.queue_free() - return result == false + assert_that(result).is_false() -func test_would_dispatch_mwt_binding() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) +func test_would_dispatch_mwt_binding() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 0) - var result: bool = bool(router.call("would_dispatch", "BD-BASS", "impact")) # requires MWT 3 - router.queue_free() - return result == false + var result: bool = bool(router.call("would_dispatch", "BD-BASS", "impact")) + assert_that(result).is_false() class MockPresenter: @@ -96,28 +69,26 @@ class MockPresenter: received = true -func test_dispatch_event_triggers_presenter() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) +func test_dispatch_event_triggers_presenter() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 3) - var presenter: MockPresenter = MockPresenter.new() + var presenter: MockPresenter = auto_free(MockPresenter.new()) router.call("set_presenter", presenter) router.call("dispatch_event", "BD-BASS", "impact") - var result: bool = presenter.received - router.queue_free() - return result + assert_that(presenter.received).is_true() -func test_dispatch_event_applies_cooldown() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) +func test_dispatch_event_applies_cooldown() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 3) @@ -125,16 +96,14 @@ func test_dispatch_event_applies_cooldown() -> bool: var cooldowns: Dictionary = router.get("_cooldowns") as Dictionary var cooldown: float = float(cooldowns.get("BD-BASS", 0.0)) - var result: bool = is_equal_approx(cooldown, 4.0) - router.queue_free() - return result + assert_that(is_equal_approx(cooldown, 4.0)).is_true() -func test_reset_cooldowns() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) +func test_reset_cooldowns() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 3) @@ -143,51 +112,34 @@ func test_reset_cooldowns() -> bool: var cooldowns: Dictionary = router.get("_cooldowns") as Dictionary var cooldown: float = float(cooldowns.get("BD-BASS", 0.0)) - router.queue_free() - return is_equal_approx(cooldown, 0.0) + assert_that(is_equal_approx(cooldown, 0.0)).is_true() -func test_logical_event_volume_agnostic() -> bool: - # Logical events should dispatch even if volume is 0. - # Our router doesn't even know about volume, so this is true by design. - var router: Node = RouterScript.new() - root.add_child(router) +func test_logical_event_volume_agnostic() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 3) - var presenter: MockPresenter = MockPresenter.new() + var presenter: MockPresenter = auto_free(MockPresenter.new()) router.call("set_presenter", presenter) router.call("dispatch_event", "BD-BASS", "impact") - var result: bool = presenter.received - router.queue_free() - return result + assert_that(presenter.received).is_true() -func test_climb_feature_mapping() -> bool: - var router: Node = RouterScript.new() - root.add_child(router) +func test_climb_feature_mapping() -> void: + var router: Node = auto_free(RouterScript.new()) + get_tree().root.add_child(router) router.call("_ready") - var bm: Node = root.get_node_or_null("BurdenManager") + var bm: Node = get_tree().root.get_node_or_null("BurdenManager") if bm: bm.set("current_mwt_level", 3) var result_expand: bool = bool(router.call("would_dispatch", "BD-CLIMB", "expand")) var result_converge: bool = bool(router.call("would_dispatch", "BD-CLIMB", "converge")) - router.queue_free() - return result_expand and result_converge - - -func _initialize() -> void: - # Mock needed autoloads for headless run - if not root.has_node("BurdenManager"): - var bm_script: GDScript = load("res://scripts/autoload/burden_manager.gd") as GDScript - var bm: Node = bm_script.new() - bm.name = "BurdenManager" - root.add_child(bm) - - call_deferred("run_all") + assert_that(result_expand and result_converge).is_true() diff --git a/tests/test_caption_system.gd b/tests/test_caption_system.gd index af72855..96ad3dc 100644 --- a/tests/test_caption_system.gd +++ b/tests/test_caption_system.gd @@ -1,303 +1,185 @@ -extends Node - -## Unit / integration tests for the CaptionManager and BurdenManager caption integration. -## Run via Godot Editor test runner or `godot --headless --script tests/test_caption_system.gd`. -## Reference: DON-225 acceptance criteria. +extends GdUnitTestSuite const CAPTION_SCRIPT := preload("res://scripts/autoload/caption_manager.gd") - const BURDEN_SCRIPT := preload("res://scripts/autoload/burden_manager.gd") -var _passed: int = 0 -var _failed: int = 0 - - -func _ready() -> void: - print("=== Caption System Test Suite (DON-225) ===") - _test_caption_manager_channels() - _test_caption_event_opacity_curves() - _test_burden_transition_captions() - _test_bd_climb_loop_phase() - _test_caption_timing_tolerance() - _test_caption_channel_isolation() - _test_normal_3_to_0_transition() - print("") - print("Results: %d passed, %d failed" % [_passed, _failed]) - if _failed > 0: - push_error("Caption system test suite had failures.") - get_tree().quit(1) - else: - get_tree().quit(0) - - -# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - -func _assert(condition: bool, msg: String) -> void: - if condition: - _passed += 1 - else: - _failed += 1 - push_error("ASSERT FAILED: %s" % msg) - -func _assertf(a: float, b: float, tolerance: float, msg: String) -> void: - var diff: float = absf(a - b) - if diff <= tolerance: - _passed += 1 - else: - _failed += 1 - push_error("ASSERT FAILED: %s (|%.4f - %.4f| = %.4f > %.4f)" % [msg, a, b, diff, tolerance]) +func test_caption_manager_channels() -> void: + var cm: Node = auto_free(CAPTION_SCRIPT.new()) + assert_that(cm.Channel.DIALOGUE).is_equal(0) + assert_that(cm.Channel.BURDEN).is_equal(1) + assert_that(cm.Channel.AMBIENT).is_equal(2) + assert_that(cm.Channel.SFX).is_equal(3) -# โ”€โ”€ Test: Channel Enum & Priority โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + assert_that(cm.CHANNEL_PRIORITY[cm.Channel.BURDEN]).is_equal(2) + assert_that(cm.CHANNEL_PRIORITY[cm.Channel.DIALOGUE]).is_equal(1) + assert_that(cm.CHANNEL_SURFACE_GROUP[cm.Channel.BURDEN]).is_equal(1) + assert_that(cm.CHANNEL_SURFACE_GROUP[cm.Channel.DIALOGUE]).is_equal(0) + assert_that(cm.CHANNEL_SURFACE_GROUP[cm.Channel.AMBIENT]).is_equal(0) -func _test_caption_manager_channels() -> void: - print("\n[Test] CaptionManager channels") - var cm: Node = CAPTION_SCRIPT.new() - _assert(cm.Channel.DIALOGUE == 0, "DIALOGUE channel enum") - _assert(cm.Channel.BURDEN == 1, "BURDEN channel enum") - _assert(cm.Channel.AMBIENT == 2, "AMBIENT channel enum") - _assert(cm.Channel.SFX == 3, "SFX channel enum") - - _assert(cm.CHANNEL_PRIORITY[cm.Channel.BURDEN] == 2, "BURDEN priority = 2") - _assert(cm.CHANNEL_PRIORITY[cm.Channel.DIALOGUE] == 1, "DIALOGUE priority = 1") - - _assert(cm.CHANNEL_SURFACE_GROUP[cm.Channel.BURDEN] == 1, "BURDEN isolated surface") - _assert(cm.CHANNEL_SURFACE_GROUP[cm.Channel.DIALOGUE] == 0, "DIALOGUE shared surface") - _assert(cm.CHANNEL_SURFACE_GROUP[cm.Channel.AMBIENT] == 0, "AMBIENT shared surface") - - print(" channel tests done") - - -# โ”€โ”€ Test: Opacity Curves โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - -func _test_caption_event_opacity_curves() -> void: - print("\n[Test] CaptionEvent opacity curves") +func test_caption_event_opacity_curves() -> void: var evt: RefCounted = CAPTION_SCRIPT.CaptionEvent.new() evt.duration_sec = 2.0 - ## INSTANT = 0 evt.curve = 0 evt.elapsed_sec = 0.0 - _assert(evt.opacity() == 1.0, "INSTANT at t=0") + assert_that(evt.opacity()).is_equal(1.0) evt.elapsed_sec = 1.0 - _assert(evt.opacity() == 1.0, "INSTANT at t=1") + assert_that(evt.opacity()).is_equal(1.0) - ## LINEAR = 1 fade-in evt.curve = 1 evt.elapsed_sec = 0.0 - _assertf(evt.opacity(), 0.0, 0.01, "LINEAR fade-in at t=0") - evt.elapsed_sec = 0.1 ## 5% of 2.0 s = 0.25 opacity in first 20% - _assertf(evt.opacity(), 0.25, 0.01, "LINEAR fade-in at t=0.1") - evt.elapsed_sec = 1.0 ## midpoint (hold) - _assertf(evt.opacity(), 1.0, 0.01, "LINEAR hold at midpoint") - evt.elapsed_sec = 1.8 ## 90% through = fade-out mid - _assertf(evt.opacity(), 0.5, 0.01, "LINEAR fade-out at t=1.8") + assert_that(is_equal_approx(evt.opacity(), 0.0)).is_true() + evt.elapsed_sec = 0.1 + assert_that(is_equal_approx(evt.opacity(), 0.25)).is_true() + evt.elapsed_sec = 1.0 + assert_that(is_equal_approx(evt.opacity(), 1.0)).is_true() + evt.elapsed_sec = 1.8 + assert_that(is_equal_approx(evt.opacity(), 0.5)).is_true() evt.elapsed_sec = 2.0 - _assertf(evt.opacity(), 0.0, 0.01, "LINEAR fade-out end") + assert_that(is_equal_approx(evt.opacity(), 0.0)).is_true() - ## EXPONENTIAL = 2 fade-in evt.curve = 2 evt.elapsed_sec = 0.0 - _assertf(evt.opacity(), 0.0, 0.01, "EXPONENTIAL at t=0") - evt.elapsed_sec = 0.3 ## end of fade-in window for 2.0 s (0.3/2.0 = 0.15 < 0.3) - ## With duration 2.0, 0.3 s is 0.15 of total, so it's still in fade-in zone - ## t/dur = 0.15 < 0.3, formula: 1 - (1 - 0.15/0.3)^2 = 1 - 0.25 = 0.75 - _assertf(evt.opacity(), 0.75, 0.01, "EXPONENTIAL fade-in mid") - evt.elapsed_sec = 1.6 ## hold zone - _assertf(evt.opacity(), 1.0, 0.01, "EXPONENTIAL hold") - - ## STEEP_EXPONENTIAL = 4 + assert_that(is_equal_approx(evt.opacity(), 0.0)).is_true() + evt.elapsed_sec = 0.3 + assert_that(is_equal_approx(evt.opacity(), 0.75)).is_true() + evt.elapsed_sec = 1.6 + assert_that(is_equal_approx(evt.opacity(), 1.0)).is_true() + evt.curve = 4 evt.elapsed_sec = 0.0 - _assertf(evt.opacity(), 0.0, 0.01, "STEEP at t=0") - evt.elapsed_sec = 0.2 ## 10% of 2.0 = first 0.1 s is linear ramp - ## t=0.2, dur=2.0, ratio=0.1. First 0.1 (0.2s) is ramp: 0.2/0.2=1.0, then exp decay - _assertf(evt.opacity(), 1.0, 0.01, "STEEP peak") + assert_that(is_equal_approx(evt.opacity(), 0.0)).is_true() + evt.elapsed_sec = 0.2 + assert_that(is_equal_approx(evt.opacity(), 1.0)).is_true() evt.elapsed_sec = 0.5 var steep_val: float = evt.opacity() - _assert(steep_val < 1.0 and steep_val > 0.0, "STEEP decay is between 0 and 1") + assert_that(steep_val < 1.0 and steep_val > 0.0).is_true() - ## LOGARITHMIC = 3 tail-off (5.0 s) evt.duration_sec = 5.0 evt.curve = 3 evt.elapsed_sec = 0.0 - _assertf(evt.opacity(), 0.0, 0.01, "LOG at t=0") - evt.elapsed_sec = 0.25 ## 5% = first 10% window, ramp to 1 - _assertf(evt.opacity(), 0.5, 0.01, "LOG ramp mid") + assert_that(is_equal_approx(evt.opacity(), 0.0)).is_true() + evt.elapsed_sec = 0.25 + assert_that(is_equal_approx(evt.opacity(), 0.5)).is_true() evt.elapsed_sec = 2.5 var log_val: float = evt.opacity() - _assert(log_val > 0.0 and log_val < 1.0, "LOG tail-off mid") + assert_that(log_val > 0.0 and log_val < 1.0).is_true() evt.elapsed_sec = 5.0 - _assertf(evt.opacity(), 0.0, 0.01, "LOG end") + assert_that(is_equal_approx(evt.opacity(), 0.0)).is_true() - print(" curve tests done") - -# โ”€โ”€ Test: BurdenManager Transition Caption Scheduling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - -func _test_burden_transition_captions() -> void: - print("\n[Test] BurdenManager transition captions") - var bm := BURDEN_SCRIPT.new() - ## Force load config (normally done in _ready) +func test_burden_transition_captions() -> void: + var bm: Node = auto_free(BURDEN_SCRIPT.new()) bm._load_burden_config() bm._burden_trigger_count = 0 bm._lifetime_trigger_count = 0 - ## Verify config loaded - _assert(bm._caption_bridge._caption_transitions.has("0_to_1"), "0_to_1 transition exists") - _assert(bm._caption_bridge._caption_transitions.has("2_to_3"), "2_to_3 transition exists") - _assert( - bm._caption_bridge._caption_transitions.has("3_to_0_emergency"), "3_to_0_emergency exists" - ) - _assert(bm._caption_bridge._caption_transitions.has("3_to_0_normal"), "3_to_0_normal exists") - _assert(bm._caption_bridge._caption_transitions.has("2_to_1"), "2_to_1 transition exists") - _assert(bm._caption_bridge._caption_transitions.has("1_to_0"), "1_to_0 transition exists") - - ## Verify offsets + assert_that(bm._caption_bridge._caption_transitions.has("0_to_1")).is_true() + assert_that(bm._caption_bridge._caption_transitions.has("2_to_3")).is_true() + assert_that(bm._caption_bridge._caption_transitions.has("3_to_0_emergency")).is_true() + assert_that(bm._caption_bridge._caption_transitions.has("3_to_0_normal")).is_true() + assert_that(bm._caption_bridge._caption_transitions.has("2_to_1")).is_true() + assert_that(bm._caption_bridge._caption_transitions.has("1_to_0")).is_true() + var t0_1: Dictionary = bm._caption_bridge._caption_transitions["0_to_1"] - _assertf(float(t0_1.get("offset_sec", 0.0)), 2.0, 0.001, "0โ†’1 offset = 2.0 s") - _assert(str(t0_1.get("curve", "")) == "EXPONENTIAL", "0โ†’1 curve = EXPONENTIAL") + assert_that(is_equal_approx(float(t0_1.get("offset_sec", 0.0)), 2.0)).is_true() + assert_that(str(t0_1.get("curve", ""))).is_equal("EXPONENTIAL") var t2_3: Dictionary = bm._caption_bridge._caption_transitions["2_to_3"] - _assertf(float(t2_3.get("offset_sec", 0.0)), 2.5, 0.001, "2โ†’3 offset = 2.5 s") - _assert(str(t2_3.get("curve", "")) == "EXPONENTIAL", "2โ†’3 curve = EXPONENTIAL") + assert_that(is_equal_approx(float(t2_3.get("offset_sec", 0.0)), 2.5)).is_true() + assert_that(str(t2_3.get("curve", ""))).is_equal("EXPONENTIAL") var t_emerg: Dictionary = bm._caption_bridge._caption_transitions["3_to_0_emergency"] - _assertf(float(t_emerg.get("offset_sec", 0.0)), 0.5, 0.001, "emergency 3โ†’0 offset = 0.5 s") - _assert( - str(t_emerg.get("curve", "")) == "STEEP_EXPONENTIAL", - "emergency 3โ†’0 curve = STEEP_EXPONENTIAL" - ) + assert_that(is_equal_approx(float(t_emerg.get("offset_sec", 0.0)), 0.5)).is_true() + assert_that(str(t_emerg.get("curve", ""))).is_equal("STEEP_EXPONENTIAL") var t2_1: Dictionary = bm._caption_bridge._caption_transitions["2_to_1"] - _assertf(float(t2_1.get("offset_sec", 0.0)), 1.0, 0.001, "2โ†’1 offset = 1.0 s") - _assert(str(t2_1.get("curve", "")) == "LINEAR", "2โ†’1 curve = LINEAR") + assert_that(is_equal_approx(float(t2_1.get("offset_sec", 0.0)), 1.0)).is_true() + assert_that(str(t2_1.get("curve", ""))).is_equal("LINEAR") var t1_0: Dictionary = bm._caption_bridge._caption_transitions["1_to_0"] - _assertf(float(t1_0.get("offset_sec", 0.0)), 1.0, 0.001, "1โ†’0 offset = 1.0 s") - _assert(str(t1_0.get("curve", "")) == "LINEAR", "1โ†’0 curve = LINEAR") + assert_that(is_equal_approx(float(t1_0.get("offset_sec", 0.0)), 1.0)).is_true() + assert_that(str(t1_0.get("curve", ""))).is_equal("LINEAR") var t_norm: Dictionary = bm._caption_bridge._caption_transitions["3_to_0_normal"] - _assertf(float(t_norm.get("offset_sec", 0.0)), 2.5, 0.001, "normal 3โ†’0 offset = 2.5 s") - _assert(str(t_norm.get("curve", "")) == "LOGARITHMIC", "normal 3โ†’0 curve = LOGARITHMIC") - _assertf(float(t_norm.get("duration_sec", 0.0)), 5.0, 0.001, "normal 3โ†’0 duration = 5.0 s") - - print(" transition caption tests done") + assert_that(is_equal_approx(float(t_norm.get("offset_sec", 0.0)), 2.5)).is_true() + assert_that(str(t_norm.get("curve", ""))).is_equal("LOGARITHMIC") + assert_that(is_equal_approx(float(t_norm.get("duration_sec", 0.0)), 5.0)).is_true() -# โ”€โ”€ Test: BD-CLIMB Loop Phase โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - -func _test_bd_climb_loop_phase() -> void: - print("\n[Test] BD-CLIMB loop-phase gate") - var cm: Node = CAPTION_SCRIPT.new() +func test_bd_climb_loop_phase() -> void: + var cm: Node = auto_free(CAPTION_SCRIPT.new()) cm._ready() cm.set_bd_climb_enabled(true) - _assert(cm._bd_climb_enabled == true, "BD-CLIMB enabled") + assert_that(cm._bd_climb_enabled).is_true() cm.report_bd_climb_width(0.25) - _assertf(cm.get_bd_climb_width(), 0.25, 0.001, "width = 0.25") - _assertf(cm.get_bd_climb_phase(), 0.25, 0.001, "phase derived from width = 0.25") + assert_that(is_equal_approx(cm.get_bd_climb_width(), 0.25)).is_true() + assert_that(is_equal_approx(cm.get_bd_climb_phase(), 0.25)).is_true() cm.report_bd_climb_phase(0.75) - _assertf(cm.get_bd_climb_phase(), 0.75, 0.001, "explicit phase = 0.75") + assert_that(is_equal_approx(cm.get_bd_climb_phase(), 0.75)).is_true() - _assert(cm.is_phase_within(0.5, 1.0) == true, "phase 0.75 within [0.5,1.0]") - _assert(cm.is_phase_within(0.0, 0.5) == false, "phase 0.75 outside [0.0,0.5]") + assert_that(cm.is_phase_within(0.5, 1.0)).is_true() + assert_that(cm.is_phase_within(0.0, 0.5)).is_false() cm.report_stem_transient("BD-CLIMB", "peak", 0.8) - _assert(cm.was_stem_transient_recent("BD-CLIMB", "peak", 1.0) == true, "stem transient recent") - _assert( - cm.was_stem_transient_recent("BD-CLIMB", "valley", 1.0) == false, - "different event id not recent" - ) - _assert( - cm.was_stem_transient_recent("BD-MECH", "peak", 1.0) == false, "different stem not recent" - ) - - ## Width captions from BurdenManager config - var bm := BURDEN_SCRIPT.new() + assert_that(cm.was_stem_transient_recent("BD-CLIMB", "peak", 1.0)).is_true() + assert_that(cm.was_stem_transient_recent("BD-CLIMB", "valley", 1.0)).is_false() + assert_that(cm.was_stem_transient_recent("BD-MECH", "peak", 1.0)).is_false() + + var bm: Node = auto_free(BURDEN_SCRIPT.new()) bm._load_burden_config() var caps: Dictionary = bm.get_bd_climb_width_captions() - _assert(caps.has("expanding"), "expanding caption exists") - _assert(caps.has("converging"), "converging caption exists") - _assert(str(caps["expanding"].get("text", "")) == "[The walls widen]", "expanding text") - _assert(str(caps["converging"].get("text", "")) == "[Everything converges]", "converging text") - - print(" BD-CLIMB tests done") + assert_that(caps.has("expanding")).is_true() + assert_that(caps.has("converging")).is_true() + assert_that(str(caps["expanding"].get("text", ""))).is_equal("[The walls widen]") + assert_that(str(caps["converging"].get("text", ""))).is_equal("[Everything converges]") -# โ”€โ”€ Test: Caption Timing Tolerance โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - -func _test_caption_timing_tolerance() -> void: - print("\n[Test] Timing tolerance (ยฑ0.2 s)") +func test_caption_timing_tolerance() -> void: var evt: RefCounted = CAPTION_SCRIPT.CaptionEvent.new() evt.offset_sec = 2.0 evt.duration_sec = 2.0 - _assert(evt.is_timing_accurate(2.0, 0.2) == true, "exact 2.0 within ยฑ0.2") - _assert(evt.is_timing_accurate(2.19, 0.2) == true, "2.19 within ยฑ0.2") - _assert(evt.is_timing_accurate(2.21, 0.2) == false, "2.21 outside ยฑ0.2") - _assert(evt.is_timing_accurate(1.81, 0.2) == true, "1.81 within ยฑ0.2") - _assert(evt.is_timing_accurate(1.79, 0.2) == false, "1.79 outside ยฑ0.2") - - print(" tolerance tests done") - - -# โ”€โ”€ Test: Channel Isolation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + assert_that(evt.is_timing_accurate(2.0, 0.2)).is_true() + assert_that(evt.is_timing_accurate(2.19, 0.2)).is_true() + assert_that(evt.is_timing_accurate(2.21, 0.2)).is_false() + assert_that(evt.is_timing_accurate(1.81, 0.2)).is_true() + assert_that(evt.is_timing_accurate(1.79, 0.2)).is_false() -func _test_caption_channel_isolation() -> void: - print("\n[Test] BURDEN channel isolation") - var cm: Node = CAPTION_SCRIPT.new() +func test_caption_channel_isolation() -> void: + var cm: Node = auto_free(CAPTION_SCRIPT.new()) cm._ready() - ## Simulate scheduling DIALOGUE then BURDEN - var dia: RefCounted = cm.schedule("Hello", 0, 0.0, 2.0) ## Channel.DIALOGUE = 0 - var bur: RefCounted = cm.schedule("[The world stills]", 1, 0.0, 2.0) ## Channel.BURDEN = 1 + var dia: RefCounted = cm.schedule("Hello", 0, 0.0, 2.0) + var bur: RefCounted = cm.schedule("[The world stills]", 1, 0.0, 2.0) - ## BURDEN should be on isolated surface group - _assert(bur.surface_group == 1, "BURDEN on isolated surface") - _assert(dia.surface_group == 0, "DIALOGUE on shared surface") + assert_that(bur.surface_group).is_equal(1) + assert_that(dia.surface_group).is_equal(0) - ## Flush to clean state cm.flush_all() - _assert(cm._active_events.is_empty(), "flush clears active") - _assert(cm._event_queue.is_empty(), "flush clears queue") + assert_that(cm._active_events.is_empty()).is_true() + assert_that(cm._event_queue.is_empty()).is_true() - print(" isolation tests done") - -# โ”€โ”€ Test: Normal 3โ†’0 Transition โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - -func _test_normal_3_to_0_transition() -> void: - print("\n[Test] Normal 3โ†’0 transition caption") - var bm := BURDEN_SCRIPT.new() +func test_normal_3_to_0_transition() -> void: + var bm: Node = auto_free(BURDEN_SCRIPT.new()) bm._load_burden_config() var data: Dictionary = bm._caption_bridge._caption_transitions.get("3_to_0_normal", {}) - _assert(not data.is_empty(), "3_to_0_normal config exists") - _assertf(float(data.get("offset_sec", 0.0)), 2.5, 0.001, "normal 3โ†’0 offset") - _assertf(float(data.get("duration_sec", 0.0)), 5.0, 0.001, "normal 3โ†’0 duration") - _assert(str(data.get("curve", "")) == "LOGARITHMIC", "normal 3โ†’0 curve") + assert_that(not data.is_empty()).is_true() + assert_that(is_equal_approx(float(data.get("offset_sec", 0.0)), 2.5)).is_true() + assert_that(is_equal_approx(float(data.get("duration_sec", 0.0)), 5.0)).is_true() + assert_that(str(data.get("curve", ""))).is_equal("LOGARITHMIC") - ## Verify BurdenManager can schedule it explicitly - var cm: Node = CAPTION_SCRIPT.new() + var cm: Node = auto_free(CAPTION_SCRIPT.new()) cm._ready() - ## We can't directly test _schedule_transition_caption without a full tree, - ## but we test the explicit API with a dummy caption node bm.schedule_transition_caption_explicit("3_to_0_normal") - ## Since cm is not at /root/CaptionManager, this should no-op gracefully - ## (no crash = pass) - _assert(true, "explicit 3โ†’0 scheduling no-ops gracefully without autoload") - - print(" normal 3โ†’0 tests done") + assert_that(true).is_true() diff --git a/tests/test_deterministic_math.gd b/tests/test_deterministic_math.gd index 223010a..71c96b7 100644 --- a/tests/test_deterministic_math.gd +++ b/tests/test_deterministic_math.gd @@ -1,98 +1,22 @@ -class_name TestDeterministicMath -extends SceneTree - -## In-engine deterministic math validation suite. -## Run this as an autoload or from a test scene to verify Tier-1 math. -## -## Validation strategy: -## 1. Golden-seed determinism (ยง11.4) -## 2. Damage formula equivalence against Python prototype edge-case bank -## 3. AP economy state-machine consistency -## 4. SHA-256 cross-platform hash stability -## -## Prints PASS/FAIL to stdout. Returns exit code 0 only if all pass. - -signal suite_finished(passed: int, failed: int) - -var _passed: int = 0 -var _failed: int = 0 -var _reports: Array[String] = [] - - -func _initialize() -> void: - run_all() - - -func run_all() -> void: - print("\n=== EMBERFALL DETERMINISTIC MATH VALIDATION ===\n") - _test_golden_seed_hash() - _test_damage_formula_100_edge_cases() - _test_ap_economy_state_machine() - _test_position_modifier_matrix() - _test_floor_clamp_edge_cases() - _test_elemental_modifiers() - _test_entity_stat_clamping() - _test_sha256_cross_platform() - - print("\n=== RESULTS ===") - print("Passed: %d" % _passed) - print("Failed: %d" % _failed) - for r: String in _reports: - print(r) - - if _failed > 0: - push_error("DETERMINISTIC MATH VALIDATION FAILED") - quit(1) - else: - print("ALL VALIDATION PASSED โ€” math is deterministic.") - if not OS.is_debug_build(): - for child in get_root().get_children(): - if ( - child.name - in [ - "ConfigLoader", - "BurdenManager", - "EntityLifecycle", - "RunManager", - "GridSystem", - "CaptionManager", - "AudioMiddleware", - "SafeZoneManager", - "LayerManager", - "ToastManager", - "FocusManager", - "SettingsManager", - "InputRouter" - ] - ): - child.queue_free() - quit(0) - - suite_finished.emit(_passed, _failed) - - -# โ”€โ”€ 1. Golden Seed Hash โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_golden_seed_hash() -> void: - var golden: int = GameConstants.GOLDEN_SEED # 0xDEADBEEF +extends GdUnitTestSuite + + +func test_golden_seed_hash() -> void: + var golden: int = GameConstants.GOLDEN_SEED var h1: int = SeedGovernance.hash_int(golden, "TEST") var h2: int = SeedGovernance.hash_int(golden, "TEST") - _assert_eq("golden_seed_repeatability", h1, h2) + assert_that(h1).is_equal(h2) var known_topo: int = SeedGovernance.seed_room_topology(golden, 0, 16) var known_enc: int = SeedGovernance.seed_encounter(golden, 0, 8) - # We don't hardcode exact expected numbers because SHA-256 values differ - # between Python hashlib and Godot HashingContext, but we verify - # that successive calls in-engine are identical (in-engine determinism). - _assert_eq("golden_seed_topo_repeat", known_topo, known_topo) - _assert_eq("golden_seed_enc_repeat", known_enc, known_enc) + assert_that(known_topo).is_equal(known_topo) + assert_that(known_enc).is_equal(known_enc) - # Verify seed validation passes for golden seed var valid: bool = SeedGovernance.validate_seed(golden, {}) - _assert_true("golden_seed_valid", valid) + assert_that(valid).is_true() -# โ”€โ”€ 2. Damage Formula: 100 Edge Cases โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_damage_formula_100_edge_cases() -> void: +func test_damage_formula_100_edge_cases() -> void: var rng: RandomNumberGenerator = RandomNumberGenerator.new() rng.seed = GameConstants.GOLDEN_SEED @@ -104,158 +28,130 @@ func _test_damage_formula_100_edge_cases() -> void: var mem: float = snappedf(rng.randf_range(0.0, 0.3), 0.01) var dmg: int = CombatFormula.compute_damage(off, def_stat, pos, elem, mem) + assert_that(dmg >= 1).is_true() + assert_that(dmg <= 9999).is_true() - # Invariants - _assert_gte("edge_damage_min_%d" % i, dmg, 1) - _assert_lte("edge_damage_max_%d" % i, dmg, 9999) - - # Determinism: calling again with same args must match var dmg2: int = CombatFormula.compute_damage(off, def_stat, pos, elem, mem) - _assert_eq("edge_determinism_%d" % i, dmg, dmg2) + assert_that(dmg).is_equal(dmg2) - # Reference cases from prototype batch_simulation.py (assuming DEF=8 for base=14) var baseline: int = CombatFormula.compute_damage(12, 8, 1.0, 1.0, 0.0) - _assert_eq("ref_baseline", baseline, 14) - _assert_eq("ref_backstab", CombatFormula.compute_damage(12, 8, 1.25, 1.0, 0.0), 17) - _assert_eq("ref_elev1", CombatFormula.compute_damage(12, 8, 1.15, 1.0, 0.0), 16) - _assert_eq("ref_elev2", CombatFormula.compute_damage(12, 8, 1.25, 1.0, 0.0), 17) - _assert_eq("ref_cover", CombatFormula.compute_damage(12, 8, 0.85, 1.0, 0.0), 11) - _assert_eq("ref_combo", CombatFormula.compute_damage(12, 8, 1.25, 2.0, 0.0), 35) - _assert_eq("ref_memory", CombatFormula.compute_damage(12, 8, 1.25, 1.0, 0.30), 22) - _assert_eq("ref_heavy_cover", CombatFormula.compute_damage(12, 8, 0.70, 1.0, 0.0), 9) - - -# โ”€โ”€ 3. AP Economy State Machine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_ap_economy_state_machine() -> void: - # Turn 1 initial AP is set to AP_MAX by game setup code. - # start_phase() is for subsequent turns based on carried-over AP. - # Scenarios from spec ยง9.2: - - # Turn 1: start 6, spend 4, carry 2, regen 2 โ†’ next start 4 + assert_that(baseline).is_equal(14) + assert_that(CombatFormula.compute_damage(12, 8, 1.25, 1.0, 0.0)).is_equal(17) + assert_that(CombatFormula.compute_damage(12, 8, 1.15, 1.0, 0.0)).is_equal(16) + assert_that(CombatFormula.compute_damage(12, 8, 1.25, 1.0, 0.0)).is_equal(17) + assert_that(CombatFormula.compute_damage(12, 8, 0.85, 1.0, 0.0)).is_equal(11) + assert_that(CombatFormula.compute_damage(12, 8, 1.25, 2.0, 0.0)).is_equal(35) + assert_that(CombatFormula.compute_damage(12, 8, 1.25, 1.0, 0.30)).is_equal(22) + assert_that(CombatFormula.compute_damage(12, 8, 0.70, 1.0, 0.0)).is_equal(9) + + +func test_ap_economy_state_machine() -> void: var start: int = 6 var spent: int = 4 - var end_ap: int = start - spent # 2 + var end_ap: int = start - spent var next: int = APEconomy.start_phase(end_ap) - _assert_eq("ap_spec_turn1_next", next, 4) + assert_that(next).is_equal(4) - # Turn 2: start 4, spend 1, carry 3, regen 2 โ†’ next start 5 start = next spent = 1 - end_ap = start - spent # 3 + end_ap = start - spent next = APEconomy.start_phase(end_ap) - _assert_eq("ap_spec_turn2_next", next, 5) + assert_that(next).is_equal(5) - # Turn 3: start 5, spend 0, carry 5, regen 2 โ†’ cap at 6 start = next spent = 0 - end_ap = start - spent # 5 + end_ap = start - spent next = APEconomy.start_phase(end_ap) - _assert_eq("ap_spec_turn3_cap", next, 6) + assert_that(next).is_equal(6) - # Turn 4: start 6, spend 6, carry 0, regen 2 โ†’ next start 2 start = next spent = 6 - end_ap = start - spent # 0 + end_ap = start - spent next = APEconomy.start_phase(end_ap) - _assert_eq("ap_spec_turn4_exhaust", next, 2) + assert_that(next).is_equal(2) -# โ”€โ”€ 4. Position Modifier Matrix โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_position_modifier_matrix() -> void: +func test_position_modifier_matrix() -> void: var p: Entity = Entity.new("P", 1, 1, 10, 5, 3, 1, 0, 0) var e: Entity = Entity.new("E", 2, 1, 10, 5, 3, 1, 0, 0) var cover: Array[Vector2i] = [] - # Frontal var mod: float = CombatFormula.calculate_position_modifier(p, e, cover) - _assert_eqf("pm_frontal", mod, 1.0) + assert_that(is_equal_approx(mod, 1.0)).is_true() - # Backstab: matches test_core_mechanic.py (attacker at (1,1), defender at (2,1), defender facing left) p.x = 1 p.y = 1 e.x = 2 e.y = 1 - e.facing_x = -1 # defender facing left; attacker-to-defender = (1,0) โ†’ dot = -1 < -0.7 + e.facing_x = -1 mod = CombatFormula.calculate_position_modifier(p, e, cover) - _assert_eqf("pm_backstab", mod, 1.25) + assert_that(is_equal_approx(mod, 1.25)).is_true() - # Elevation +1 p.elevation = 1 e.elevation = 0 p.facing_x = 1 - e.facing_x = 1 # defender facing same as attacker (away) โ†’ dot = 1, NO backstab + e.facing_x = 1 mod = CombatFormula.calculate_position_modifier(p, e, cover) - _assert_eqf("pm_elev1", mod, 1.15) + assert_that(is_equal_approx(mod, 1.15)).is_true() - # Elevation +2 p.elevation = 2 mod = CombatFormula.calculate_position_modifier(p, e, cover) - _assert_eqf("pm_elev2", mod, 1.25) + assert_that(is_equal_approx(mod, 1.25)).is_true() - # Light cover p.elevation = 0 cover = [Vector2i(2, 1)] mod = CombatFormula.calculate_position_modifier(p, e, cover) - _assert_eqf("pm_light_cover", mod, 0.85) + assert_that(is_equal_approx(mod, 0.85)).is_true() - # Heavy cover (adjacent cover tile) cover = [Vector2i(2, 1), Vector2i(3, 1)] mod = CombatFormula.calculate_position_modifier(p, e, cover) - _assert_eqf("pm_heavy_cover", mod, 0.70) + assert_that(is_equal_approx(mod, 0.70)).is_true() - # Elevation penalty (defender higher) cover = [] p.elevation = 0 e.elevation = 2 - e.facing_x = 1 # ensure no backstab + e.facing_x = 1 mod = CombatFormula.calculate_position_modifier(p, e, cover) - _assert_eqf("pm_elev_penalty", mod, 0.75) + assert_that(is_equal_approx(mod, 0.75)).is_true() -# โ”€โ”€ 5. Floor / Clamp Edge Cases โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_floor_clamp_edge_cases() -> void: - _assert_eq("floor_exact", DeterministicMath.floori(14.0), 14) - _assert_eq("floor_half", DeterministicMath.floori(17.5), 17) - _assert_eq("floor_decimal", DeterministicMath.floori(21.7), 21) - _assert_eq("floor_small", DeterministicMath.floori(9.8), 9) - _assert_eq("floor_zero", DeterministicMath.floori(0.0), 0) - _assert_eq("floor_neg", DeterministicMath.floori(-2.3), -3) +func test_floor_clamp_edge_cases() -> void: + assert_that(DeterministicMath.floori(14.0)).is_equal(14) + assert_that(DeterministicMath.floori(17.5)).is_equal(17) + assert_that(DeterministicMath.floori(21.7)).is_equal(21) + assert_that(DeterministicMath.floori(9.8)).is_equal(9) + assert_that(DeterministicMath.floori(0.0)).is_equal(0) + assert_that(DeterministicMath.floori(-2.3)).is_equal(-3) - _assert_eq("clampf_upper", DeterministicMath.clampf(1.55, 0.5, 1.5), 1.5) - _assert_eq("clampf_lower", DeterministicMath.clampf(0.45, 0.5, 1.5), 0.5) - _assert_eq("clampi_mid", DeterministicMath.clampi(7, 0, 10), 7) - _assert_eq("damage_floor_pos", DeterministicMath.damage_floor(14.0), 14) - _assert_eq("damage_floor_zero", DeterministicMath.damage_floor(0.0), 1) - _assert_eq("damage_floor_neg", DeterministicMath.damage_floor(-3.0), 1) + assert_that(is_equal_approx(DeterministicMath.clampf(1.55, 0.5, 1.5), 1.5)).is_true() + assert_that(is_equal_approx(DeterministicMath.clampf(0.45, 0.5, 1.5), 0.5)).is_true() + assert_that(DeterministicMath.clampi(7, 0, 10)).is_equal(7) + assert_that(DeterministicMath.damage_floor(14.0)).is_equal(14) + assert_that(DeterministicMath.damage_floor(0.0)).is_equal(1) + assert_that(DeterministicMath.damage_floor(-3.0)).is_equal(1) -# โ”€โ”€ 6. Elemental Modifiers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_elemental_modifiers() -> void: - _assert_eqf("elem_fire_to_oil", CombatFormula.elemental_modifier("fire_to_oil"), 2.0) - _assert_eqf("elem_wind_to_fire", CombatFormula.elemental_modifier("wind_to_fire"), 1.5) - _assert_eqf("elem_oil_slip", CombatFormula.elemental_modifier("oil_slip"), 0.8) - _assert_eqf("elem_water_to_fire", CombatFormula.elemental_modifier("water_to_fire"), 0.5) - _assert_eqf("elem_unknown", CombatFormula.elemental_modifier("none"), 1.0) +func test_elemental_modifiers() -> void: + assert_that(is_equal_approx(CombatFormula.elemental_modifier("fire_to_oil"), 2.0)).is_true() + assert_that(is_equal_approx(CombatFormula.elemental_modifier("wind_to_fire"), 1.5)).is_true() + assert_that(is_equal_approx(CombatFormula.elemental_modifier("oil_slip"), 0.8)).is_true() + assert_that(is_equal_approx(CombatFormula.elemental_modifier("water_to_fire"), 0.5)).is_true() + assert_that(is_equal_approx(CombatFormula.elemental_modifier("none"), 1.0)).is_true() -# โ”€โ”€ 7. Entity Stat Clamping โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_entity_stat_clamping() -> void: +func test_entity_stat_clamping() -> void: var ent: Entity = Entity.new("Test", 0, 0, 500, 50, 30) ent.hp = -10 - _assert_eq("clamp_hp_neg", ent.hp, 0) + assert_that(ent.hp).is_equal(0) ent.hp = 10000 - _assert_eq("clamp_hp_over", ent.hp, 500) + assert_that(ent.hp).is_equal(500) ent.off = -5 - _assert_eq("clamp_off_neg", ent.off, 0) + assert_that(ent.off).is_equal(0) ent.off = 2000 - _assert_eq("clamp_off_over", ent.off, 999) + assert_that(ent.off).is_equal(999) -# โ”€โ”€ 8. SHA-256 Cross-Platform โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _test_sha256_cross_platform() -> void: - # These are *known* reference inputs. The exact Godot HashingContext - # output will be recorded by validate_gdscript_math.py on first run, - # then compared across platforms. +func test_sha256_cross_platform() -> void: var inputs: Array[String] = [ "0xDEADBEEFTEST", "12345TOPO0", @@ -265,67 +161,16 @@ func _test_sha256_cross_platform() -> void: for s: String in inputs: results.append(SeedGovernance.hash_seed(s)) - # In-engine repeatability for i: int in range(inputs.size()): var v2: int = SeedGovernance.hash_seed(inputs[i]) - _assert_eq("sha256_repeat_%d" % i, results[i], v2) + assert_that(results[i]).is_equal(v2) - # Verify golden seed generation path var topo: int = SeedGovernance.seed_room_topology(GameConstants.GOLDEN_SEED, 0, 16) var enc: int = SeedGovernance.seed_encounter(GameConstants.GOLDEN_SEED, 0, 8) - var echo_: int = SeedGovernance.seed_echo(GameConstants.GOLDEN_SEED, 0, 4) - _assert_gte("golden_topo_range", topo, 0) - _assert_lt("golden_topo_range", topo, 16) - _assert_gte("golden_enc_range", enc, 0) - _assert_lt("golden_enc_range", enc, 8) - _assert_gte("golden_echo_range", echo_, 0) - _assert_lt("golden_echo_range", echo_, 4) - - -# โ”€โ”€ Assertions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _assert_eq(name: String, a: int, b: int) -> void: - if a == b: - _passed += 1 - else: - _failed += 1 - _reports.append("[FAIL] %s: expected %d, got %d" % [name, b, a]) - - -func _assert_eqf(name: String, a: float, b: float) -> void: - if abs(a - b) < 0.001: - _passed += 1 - else: - _failed += 1 - _reports.append("[FAIL] %s: expected %.3f, got %.3f" % [name, b, a]) - - -func _assert_true(name: String, cond: bool) -> void: - if cond: - _passed += 1 - else: - _failed += 1 - _reports.append("[FAIL] %s: expected true" % name) - - -func _assert_gte(name: String, a: int, b: int) -> void: - if a >= b: - _passed += 1 - else: - _failed += 1 - _reports.append("[FAIL] %s: expected %d >= %d" % [name, a, b]) - - -func _assert_lte(name: String, a: int, b: int) -> void: - if a <= b: - _passed += 1 - else: - _failed += 1 - _reports.append("[FAIL] %s: expected %d <= %d" % [name, a, b]) - - -func _assert_lt(name: String, a: int, b: int) -> void: - if a < b: - _passed += 1 - else: - _failed += 1 - _reports.append("[FAIL] %s: expected %d < %d" % [name, a, b]) + var echo_val: int = SeedGovernance.seed_echo(GameConstants.GOLDEN_SEED, 0, 4) + assert_that(topo >= 0).is_true() + assert_that(topo < 16).is_true() + assert_that(enc >= 0).is_true() + assert_that(enc < 8).is_true() + assert_that(echo_val >= 0).is_true() + assert_that(echo_val < 4).is_true() diff --git a/tests/test_elemental_resolver.gd b/tests/test_elemental_resolver.gd index 47736bf..d156d4e 100644 --- a/tests/test_elemental_resolver.gd +++ b/tests/test_elemental_resolver.gd @@ -1,62 +1,6 @@ -extends SceneTree -## Unit / integration tests for Elemental Interaction Resolver (DON-101 B3). -## -## Acceptance Criteria: -## AC-1: Fire + Oil interaction produces 2.0ร— damage modifier -## AC-2: Wind applied to Fire produces 1.5ร— modifier and spread behaviour -## AC-3: Water applied to Fire produces 0.5ร— modifier and extinguishes -## AC-4: Oil slip terrain effect produces 0.8ร— movement speed debuff -## AC-5: Duration tracking and deterministic modifier application -## AC-6: Combo chains resolve in correct order (FIFO) -## AC-7: Edge cases (multiple overlapping elements) handled gracefully -## -## Run via: godot --headless --path . -s tests/test_elemental_resolver.gd - -var _passed: int = 0 -var _failed: int = 0 - - -func _initialize() -> void: - run_all() - quit(0 if _failed == 0 else 1) - - -func run_all() -> void: - print("\n=== EMBERFALL ELEMENTAL RESOLVER TESTS ===\n") - - var tests: Array[String] = [ - "test_fire_oil_modifier", - "test_wind_fire_modifier", - "test_water_fire_modifier", - "test_oil_slip_speed", - "test_no_elements_default", - "test_duration_tracking_expiry", - "test_fifo_water_before_fire", - "test_fifo_fire_then_oil_then_wind", - "test_fire_spread_basic", - "test_spread_blocked_by_water", - "test_out_of_bounds_spread_rejected", - "test_multiple_overlapping_elements", - "test_oil_burns_off_completely", - "test_extinguish_bidirectional", - "test_stacked_elements_tick_independently", - "test_empty_effects_safe", - "test_turn_tick_idempotent", - ] - - for name: String in tests: - print("Running %s ..." % name) - call(name) - - print("\n=== RESULTS ===") - print("Passed: %d" % _passed) - print("Failed: %d" % _failed) - - if _failed > 0: - push_error("ELEMENTAL RESOLVER TESTS FAILED") - - -# โ”€โ”€ AC-1: Fireโ†”Oil = 2.0ร— โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +extends GdUnitTestSuite + + func test_fire_oil_modifier() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -67,10 +11,9 @@ func test_fire_oil_modifier() -> void: ) var mult: float = ElementalInteractionResolver.compute_tile_damage_multiplier(effects, 0) - _assert_eqf("fire_oil_damage_mult", mult, 2.0) + assert_that(is_equal_approx(mult, 2.0)).is_true() -# โ”€โ”€ AC-2: Windโ†’Fire = 1.5ร— โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_wind_fire_modifier() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -81,10 +24,9 @@ func test_wind_fire_modifier() -> void: ) var mult: float = ElementalInteractionResolver.compute_tile_damage_multiplier(effects, 0) - _assert_eqf("wind_fire_damage_mult", mult, 1.5) + assert_that(is_equal_approx(mult, 1.5)).is_true() -# โ”€โ”€ AC-3: Waterโ†’Fire = 0.5ร— and extinguish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_water_fire_modifier() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -95,10 +37,9 @@ func test_water_fire_modifier() -> void: ) var mult: float = ElementalInteractionResolver.compute_tile_damage_multiplier(effects, 0) - _assert_eqf("water_fire_damage_mult", mult, 0.5) + assert_that(is_equal_approx(mult, 0.5)).is_true() -# โ”€โ”€ AC-4: Oil slip = 0.8ร— speed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_oil_slip_speed() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -106,42 +47,36 @@ func test_oil_slip_speed() -> void: ) var speed: float = ElementalInteractionResolver.calculate_movement_speed_multiplier(effects, 0) - _assert_eqf("oil_slip_speed", speed, 0.8) + assert_that(is_equal_approx(speed, 0.8)).is_true() -# โ”€โ”€ Default: no elements = 1.0ร— โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_no_elements_default() -> void: var effects: Array[ElementalTypes.TileEffect] = [] var dmg: float = ElementalInteractionResolver.compute_tile_damage_multiplier(effects, 0) var spd: float = ElementalInteractionResolver.calculate_movement_speed_multiplier(effects, 0) - _assert_eqf("no_elem_damage", dmg, 1.0) - _assert_eqf("no_elem_speed", spd, 1.0) + assert_that(is_equal_approx(dmg, 1.0)).is_true() + assert_that(is_equal_approx(spd, 1.0)).is_true() -# โ”€โ”€ AC-5: Duration tracking and expiry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_duration_tracking_expiry() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( effects, ElementalTypes.ElementType.FIRE, 0, 1 ) - # Turn 0: still active (applied_turn 0 + duration 1 = 1; current_turn 0 โ‰ค 1) var active_turn0: Array[ElementalTypes.TileEffect] = ( ElementalInteractionResolver._filter_active(effects, 0) ) - _assert_eq("duration_active_turn0", active_turn0.size(), 1) + assert_that(active_turn0.size()).is_equal(1) - # Turn 1: expired (current_turn 1 > 0 + 1) var active_turn1: Array[ElementalTypes.TileEffect] = ( ElementalInteractionResolver._filter_active(effects, 2) ) - _assert_eq("duration_expired_turn2", active_turn1.size(), 0) + assert_that(active_turn1.size()).is_equal(0) -# โ”€โ”€ AC-6: FIFO โ€” Water applied before Fire should still extinguish โ”€โ”€โ”€ func test_fifo_water_before_fire() -> void: var effects: Array[ElementalTypes.TileEffect] = [] - # Water first (older), Fire second (newer) effects = ElementalInteractionResolver.apply_element( effects, ElementalTypes.ElementType.WATER, 0, 2 ) @@ -155,20 +90,11 @@ func test_fifo_water_before_fire() -> void: var out_effects: Array[ElementalTypes.TileEffect] = result["effects"] var extinguished: bool = result["extinguished"] - _assert_true("fifo_water_then_fire_extinguished", extinguished) - _assert_eq( - "fifo_water_then_fire_no_fire_left", - _count_element(out_effects, ElementalTypes.ElementType.FIRE), - 0 - ) - _assert_eq( - "fifo_water_then_fire_no_water_left", - _count_element(out_effects, ElementalTypes.ElementType.WATER), - 0 - ) + assert_that(extinguished).is_true() + assert_that(_count_element(out_effects, ElementalTypes.ElementType.FIRE)).is_equal(0) + assert_that(_count_element(out_effects, ElementalTypes.ElementType.WATER)).is_equal(0) -# โ”€โ”€ AC-6: FIFO chain โ€” Fire โ†’ Oil โ†’ Wind โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_fifo_fire_then_oil_then_wind() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -186,22 +112,11 @@ func test_fifo_fire_then_oil_then_wind() -> void: ) var out_effects: Array[ElementalTypes.TileEffect] = result["effects"] - # Fire first, then Oil: Fire consumes Oil in FIFO; then Wind fans Fire - # and is consumed by the interaction (wind does not persist after fanning). - _assert_eq( - "fifo_chain_oil_burned", _count_element(out_effects, ElementalTypes.ElementType.OIL), 0 - ) - # Fire should remain after burning oil and being fanned - _assert_eq( - "fifo_chain_fire_remains", _count_element(out_effects, ElementalTypes.ElementType.FIRE), 1 - ) - # Wind is consumed when it fans fire (it does not persist) - _assert_eq( - "fifo_chain_wind_consumed", _count_element(out_effects, ElementalTypes.ElementType.WIND), 0 - ) + assert_that(_count_element(out_effects, ElementalTypes.ElementType.OIL)).is_equal(0) + assert_that(_count_element(out_effects, ElementalTypes.ElementType.FIRE)).is_equal(1) + assert_that(_count_element(out_effects, ElementalTypes.ElementType.WIND)).is_equal(0) -# โ”€โ”€ AC-2: Fire spread basic โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_fire_spread_basic() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -217,15 +132,13 @@ func test_fire_spread_basic() -> void: ) var spread: Array[Vector2i] = result["spread_positions"] - # From (1,1) with wind fanning fire, spread to 4 adjacent cardinal tiles - _assert_eq("spread_count", spread.size(), 4) - _assert_true("spread_has_0_1", spread.has(Vector2i(0, 1))) - _assert_true("spread_has_2_1", spread.has(Vector2i(2, 1))) - _assert_true("spread_has_1_0", spread.has(Vector2i(1, 0))) - _assert_true("spread_has_1_2", spread.has(Vector2i(1, 2))) + assert_that(spread.size()).is_equal(4) + assert_that(spread.has(Vector2i(0, 1))).is_true() + assert_that(spread.has(Vector2i(2, 1))).is_true() + assert_that(spread.has(Vector2i(1, 0))).is_true() + assert_that(spread.has(Vector2i(1, 2))).is_true() -# โ”€โ”€ AC-7: Spread blocked by water tiles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_spread_blocked_by_water() -> void: var fire_pos := Vector2i(1, 1) var bounds: Array[Vector2i] = [Vector2i(0, 0), Vector2i(2, 2)] @@ -235,14 +148,13 @@ func test_spread_blocked_by_water() -> void: fire_pos, bounds, water_tiles ) - _assert_eq("spread_blocked_count", targets.size(), 2) - _assert_true("spread_blocked_has_0_1", targets.has(Vector2i(0, 1))) - _assert_true("spread_blocked_has_1_2", targets.has(Vector2i(1, 2))) - _assert_false("spread_blocked_no_2_1", targets.has(Vector2i(2, 1))) - _assert_false("spread_blocked_no_1_0", targets.has(Vector2i(1, 0))) + assert_that(targets.size()).is_equal(2) + assert_that(targets.has(Vector2i(0, 1))).is_true() + assert_that(targets.has(Vector2i(1, 2))).is_true() + assert_that(targets.has(Vector2i(2, 1))).is_false() + assert_that(targets.has(Vector2i(1, 0))).is_false() -# โ”€โ”€ AC-7: Out-of-bounds spread rejected โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_out_of_bounds_spread_rejected() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -252,17 +164,15 @@ func test_out_of_bounds_spread_rejected() -> void: effects, ElementalTypes.ElementType.WIND, 0, 2 ) - # Tile at (0,0) with bounds [0,0] to [0,0] โ€” no room to spread var bounds: Array[Vector2i] = [Vector2i(0, 0), Vector2i(0, 0)] var result: Dictionary = ElementalInteractionResolver.process_turn_tick( effects, 0, Vector2i(0, 0), bounds ) var spread: Array[Vector2i] = result["spread_positions"] - _assert_eq("oob_spread_count", spread.size(), 0) + assert_that(spread.size()).is_equal(0) -# โ”€โ”€ AC-7: Multiple overlapping elements โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_multiple_overlapping_elements() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -275,29 +185,19 @@ func test_multiple_overlapping_elements() -> void: effects, ElementalTypes.ElementType.WATER, 0, 2 ) - # Water extinguishes Fire first (priority 1); Oil remains var mult: float = ElementalInteractionResolver.compute_tile_damage_multiplier(effects, 0) - _assert_eqf("multiple_water_priority", mult, 0.5) + assert_that(is_equal_approx(mult, 0.5)).is_true() - # Process tick: Fire (oldest) burns Oil first; Water then extinguishes Fire. - # Per FIFO ordering, Oil is consumed by Fire before Water gets to act. var result: Dictionary = ElementalInteractionResolver.process_turn_tick( effects, 0, Vector2i(0, 0), [] ) var out: Array[ElementalTypes.TileEffect] = result["effects"] - _assert_eq( - "multiple_after_tick_fire_gone", _count_element(out, ElementalTypes.ElementType.FIRE), 0 - ) - _assert_eq( - "multiple_after_tick_water_gone", _count_element(out, ElementalTypes.ElementType.WATER), 0 - ) - _assert_eq( - "multiple_after_tick_oil_gone", _count_element(out, ElementalTypes.ElementType.OIL), 0 - ) - _assert_true("multiple_after_tick_all_consumed", out.is_empty()) + assert_that(_count_element(out, ElementalTypes.ElementType.FIRE)).is_equal(0) + assert_that(_count_element(out, ElementalTypes.ElementType.WATER)).is_equal(0) + assert_that(_count_element(out, ElementalTypes.ElementType.OIL)).is_equal(0) + assert_that(out.is_empty()).is_true() -# โ”€โ”€ AC-7: Oil burns off completely โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_oil_burns_off_completely() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -312,14 +212,11 @@ func test_oil_burns_off_completely() -> void: ) var out: Array[ElementalTypes.TileEffect] = result["effects"] - # Oil is consumed entirely by Fire in FIFO - _assert_eq("oil_burns_off", _count_element(out, ElementalTypes.ElementType.OIL), 0) - _assert_eq("oil_burns_fire_remains", _count_element(out, ElementalTypes.ElementType.FIRE), 1) + assert_that(_count_element(out, ElementalTypes.ElementType.OIL)).is_equal(0) + assert_that(_count_element(out, ElementalTypes.ElementType.FIRE)).is_equal(1) -# โ”€โ”€ AC-7: Extinguish works bidirectionally โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_extinguish_bidirectional() -> void: - # Case A: Fire first, then Water var effects_a: Array[ElementalTypes.TileEffect] = [] effects_a = ElementalInteractionResolver.apply_element( effects_a, ElementalTypes.ElementType.FIRE, 0, 2 @@ -331,14 +228,9 @@ func test_extinguish_bidirectional() -> void: var result_a: Dictionary = ElementalInteractionResolver.process_turn_tick( effects_a, 0, Vector2i(0, 0), [] ) - _assert_true("extinguish_fire_then_water", result_a["extinguished"]) - _assert_eq( - "extinguish_fire_then_water_fire_gone", - _count_element(result_a["effects"], ElementalTypes.ElementType.FIRE), - 0 - ) + assert_that(result_a["extinguished"]).is_true() + assert_that(_count_element(result_a["effects"], ElementalTypes.ElementType.FIRE)).is_equal(0) - # Case B: Water first, then Fire var effects_b: Array[ElementalTypes.TileEffect] = [] effects_b = ElementalInteractionResolver.apply_element( effects_b, ElementalTypes.ElementType.WATER, 0, 2 @@ -350,15 +242,10 @@ func test_extinguish_bidirectional() -> void: var result_b: Dictionary = ElementalInteractionResolver.process_turn_tick( effects_b, 0, Vector2i(0, 0), [] ) - _assert_true("extinguish_water_then_fire", result_b["extinguished"]) - _assert_eq( - "extinguish_water_then_fire_fire_gone", - _count_element(result_b["effects"], ElementalTypes.ElementType.FIRE), - 0 - ) + assert_that(result_b["extinguished"]).is_true() + assert_that(_count_element(result_b["effects"], ElementalTypes.ElementType.FIRE)).is_equal(0) -# โ”€โ”€ AC-5: Stacked elements tick independently โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_stacked_elements_tick_independently() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -371,30 +258,23 @@ func test_stacked_elements_tick_independently() -> void: effects, ElementalTypes.ElementType.OIL, 0, 3 ) - # Turn 1: first Fire expires (applied 0 + duration 1 = 1; current_turn 1 > 1? No, 1 > 1 is false) - # Wait: is_expired returns current_turn > applied_turn + duration - # applied_turn 0, duration 1: expired when current_turn > 1, so turn 2+ - var active_t1: Array[ElementalTypes.TileEffect] = ElementalInteractionResolver._filter_active( effects, 1 ) - _assert_eq("stack_t1_count", active_t1.size(), 3) + assert_that(active_t1.size()).is_equal(3) var active_t2: Array[ElementalTypes.TileEffect] = ElementalInteractionResolver._filter_active( effects, 2 ) - # Fire1 expired, Fire2 and Oil remain - _assert_eq("stack_t2_count", active_t2.size(), 2) + assert_that(active_t2.size()).is_equal(2) var active_t3: Array[ElementalTypes.TileEffect] = ElementalInteractionResolver._filter_active( effects, 3 ) - # Fire2 expired, Oil remains - _assert_eq("stack_t3_count", active_t3.size(), 1) - _assert_eq("stack_t3_is_oil", active_t3[0].element, ElementalTypes.ElementType.OIL) + assert_that(active_t3.size()).is_equal(1) + assert_that(active_t3[0].element).is_equal(ElementalTypes.ElementType.OIL) -# โ”€โ”€ AC-7: Empty effects safe โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_empty_effects_safe() -> void: var effects: Array[ElementalTypes.TileEffect] = [] var mult: float = ElementalInteractionResolver.compute_tile_damage_multiplier(effects, 0) @@ -402,14 +282,13 @@ func test_empty_effects_safe() -> void: var result: Dictionary = ElementalInteractionResolver.process_turn_tick( effects, 0, Vector2i(0, 0), [] ) - _assert_eqf("empty_damage", mult, 1.0) - _assert_eqf("empty_speed", spd, 1.0) - _assert_eq("empty_effects", result["effects"].size(), 0) - _assert_eq("empty_spread", result["spread_positions"].size(), 0) - _assert_false("empty_extinguished", result["extinguished"]) + assert_that(is_equal_approx(mult, 1.0)).is_true() + assert_that(is_equal_approx(spd, 1.0)).is_true() + assert_that(result["effects"].size()).is_equal(0) + assert_that(result["spread_positions"].size()).is_equal(0) + assert_that(result["extinguished"]).is_false() -# โ”€โ”€ AC-5: Turn tick idempotent โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ func test_turn_tick_idempotent() -> void: var effects: Array[ElementalTypes.TileEffect] = [] effects = ElementalInteractionResolver.apply_element( @@ -426,17 +305,11 @@ func test_turn_tick_idempotent() -> void: effects, 0, Vector2i(0, 0), [] ) - # Same inputs should produce same outputs - _assert_eq("idempotent_effect_count", result1["effects"].size(), result2["effects"].size()) - _assert_eq( - "idempotent_spread_count", - result1["spread_positions"].size(), - result2["spread_positions"].size() - ) - _assert_eq("idempotent_extinguished", result1["extinguished"], result2["extinguished"]) + assert_that(result1["effects"].size()).is_equal(result2["effects"].size()) + assert_that(result1["spread_positions"].size()).is_equal(result2["spread_positions"].size()) + assert_that(result1["extinguished"]).is_equal(result2["extinguished"]) -# โ”€โ”€ Helper: count elements of a given type in effect list โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ static func _count_element( effects: Array[ElementalTypes.TileEffect], elem: ElementalTypes.ElementType ) -> int: @@ -445,48 +318,3 @@ static func _count_element( if e.element == elem: c += 1 return c - - -# โ”€โ”€ Assertions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _assert_eq(name: String, a: int, b: int) -> void: - if a == b: - _passed += 1 - print(" PASS: %s" % name) - else: - _failed += 1 - var msg := "[FAIL] %s: expected %d, got %d" % [name, b, a] - push_error(msg) - print(" " + msg) - - -func _assert_eqf(name: String, a: float, b: float) -> void: - if abs(a - b) < 0.001: - _passed += 1 - print(" PASS: %s" % name) - else: - _failed += 1 - var msg := "[FAIL] %s: expected %.3f, got %.3f" % [name, b, a] - push_error(msg) - print(" " + msg) - - -func _assert_true(name: String, cond: bool) -> void: - if cond: - _passed += 1 - print(" PASS: %s" % name) - else: - _failed += 1 - var msg := "[FAIL] %s: expected true" % name - push_error(msg) - print(" " + msg) - - -func _assert_false(name: String, cond: bool) -> void: - if not cond: - _passed += 1 - print(" PASS: %s" % name) - else: - _failed += 1 - var msg := "[FAIL] %s: expected false" % name - push_error(msg) - print(" " + msg) diff --git a/tests/test_entity_lifecycle.gd b/tests/test_entity_lifecycle.gd index 76a1e65..2174a55 100644 --- a/tests/test_entity_lifecycle.gd +++ b/tests/test_entity_lifecycle.gd @@ -1,136 +1,51 @@ -extends SceneTree -## Unit / integration tests for Entity Lifecycle (DON-100 B2). -## Run via Godot Editor test runner or `godot --headless --script tests/test_entity_lifecycle.gd`. -# -## Covers: -## AC-1: MORAL_FLAG increments/decrements per spec ยง4.3 -## AC-2: Burden Event fires at MWT = 3 -## AC-3: State transitions deterministic and reversible where required - - -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_damage_transitions_to_dying", - "test_heal_reverses_dying", - "test_stun_timer_resolves_to_idle", - "test_dying_timer_resolves_to_dead", - "test_spare_transitions_to_ghost", - "test_process_kill_queues_moral_delta", - "test_resolve_moral_queue_increments_flag", - "test_mwt_fires_at_three", - "test_mwt_queues_remaining_deltas", - "test_spare_applies_negative_delta", - "test_reset_clears_queue_and_timers", - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL (returned %s)" % str(ok)) - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - push_error("_EntityLifecycle test suite had failures.") - quit(1) - else: - quit(0) - - -# โ”€โ”€ Test harness helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +class_name TestEntityLifecycle +extends GdUnitTestSuite func _new_lifecycle() -> _EntityLifecycle: - # Add required autoloads to the root if they aren't there (for headless script runs) - if not root.has_node("ConfigLoader"): - var cl_script: GDScript = load("res://scripts/autoload/config_loader.gd") - var cl: Node = cl_script.new() - cl.name = "ConfigLoader" - root.add_child(cl) - - if not root.has_node("BurdenManager"): - var bm_script: GDScript = load("res://scripts/autoload/burden_manager.gd") - var bm: Node = bm_script.new() - bm.name = "BurdenManager" - root.add_child(bm) - var script: GDScript = load("res://scripts/entities/entity_lifecycle.gd") - var el: _EntityLifecycle = script.new() as _EntityLifecycle - root.add_child(el) + var el: _EntityLifecycle = auto_free(script.new()) as _EntityLifecycle + add_child(el) return el -# โ”€โ”€ AC-1: Damage applies and transitions to DYING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_damage_transitions_to_dying() -> bool: +func test_damage_transitions_to_dying() -> void: var el: _EntityLifecycle = _new_lifecycle() var enemy: Entity = Entity.new("TestEnemy", 0, 0, 10, 5, 3) el.apply_damage(null, enemy, 10) - if enemy.hp != 0: - push_error("Expected HP 0, got %d" % enemy.hp) - return false - if enemy.state != Entity.State.DYING: - push_error("Expected DYING, got %d" % enemy.state) - return false - return true + assert_that(enemy.hp).is_equal(0) + assert_that(enemy.state).is_equal(Entity.State.DYING) -# โ”€โ”€ AC-1: Heal reverses DYING โ†’ IDLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_heal_reverses_dying() -> bool: +func test_heal_reverses_dying() -> void: var el: _EntityLifecycle = _new_lifecycle() var enemy: Entity = Entity.new("TestEnemy", 0, 0, 10, 5, 3) el.apply_damage(null, enemy, 10) - if enemy.state != Entity.State.DYING: - push_error("Expected DYING before heal") - return false + assert_that(enemy.state).is_equal(Entity.State.DYING) el.heal(enemy, 5) - if enemy.hp != 5: - push_error("Expected HP 5 after heal, got %d" % enemy.hp) - return false - if enemy.state != Entity.State.IDLE: - push_error("Expected IDLE after heal, got %d" % enemy.state) - return false - return true + assert_that(enemy.hp).is_equal(5) + assert_that(enemy.state).is_equal(Entity.State.IDLE) -# โ”€โ”€ AC-3: Stun timer resolves STUNNED โ†’ IDLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_stun_timer_resolves_to_idle() -> bool: +func test_stun_timer_resolves_to_idle() -> void: var el: _EntityLifecycle = _new_lifecycle() var enemy: Entity = Entity.new("TestEnemy", 0, 0, 10, 5, 3) el.stun(enemy, 1) - if enemy.state != Entity.State.STUNNED: - push_error("Expected STUNNED") - return false + assert_that(enemy.state).is_equal(Entity.State.STUNNED) el.process_end_of_turn() - if enemy.state != Entity.State.IDLE: - push_error("Expected IDLE after stun timer, got %d" % enemy.state) - return false - return true + assert_that(enemy.state).is_equal(Entity.State.IDLE) -# โ”€โ”€ AC-3: Dying timer resolves DYING โ†’ DEAD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_dying_timer_resolves_to_dead() -> bool: +func test_dying_timer_resolves_to_dead() -> void: var el: _EntityLifecycle = _new_lifecycle() var enemy: Entity = Entity.new("TestEnemy", 0, 0, 10, 5, 3) el.apply_damage(null, enemy, 10) - if enemy.state != Entity.State.DYING: - push_error("Expected DYING") - return false + assert_that(enemy.state).is_equal(Entity.State.DYING) el.process_end_of_turn() - if enemy.state != Entity.State.DEAD: - push_error("Expected DEAD after dying timer, got %d" % enemy.state) - return false - return true + assert_that(enemy.state).is_equal(Entity.State.DEAD) -# โ”€โ”€ AC-3: Spare transitions DYING โ†’ GHOST โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_spare_transitions_to_ghost() -> bool: +func test_spare_transitions_to_ghost() -> void: var el: _EntityLifecycle = _new_lifecycle() var player: Entity = Entity.new("Player", 0, 0, 40, 12, 6) player.is_player = true @@ -140,20 +55,13 @@ func test_spare_transitions_to_ghost() -> bool: var enemy: Entity = Entity.new("TestEnemy", 0, 0, 10, 5, 3) el.apply_damage(null, enemy, 10) var ok: bool = el.spare_entity(player, enemy) - if not ok: - push_error("spare_entity returned false") - return false - if enemy.state != Entity.State.GHOST: - push_error("Expected GHOST, got %d" % enemy.state) - return false - if player.ap != 2: - push_error("Expected AP 2 after spare, got %d" % player.ap) - return false - return true - - -# โ”€โ”€ AC-1: process_kill queues a delta โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_process_kill_queues_moral_delta() -> bool: + + assert_that(ok).is_true() + assert_that(enemy.state).is_equal(Entity.State.GHOST) + assert_that(player.ap).is_equal(2) + + +func test_process_kill_queues_moral_delta() -> void: var el: _EntityLifecycle = _new_lifecycle() var player: Entity = Entity.new("Player", 0, 0, 40, 12, 6) player.is_player = true @@ -162,14 +70,10 @@ func test_process_kill_queues_moral_delta() -> bool: var enemy: Entity = Entity.new("TestEnemy", 0, 0, 10, 5, 3) el.apply_damage(null, enemy, 10) el.process_kill(player, enemy, true, "enemy_01", "Grunt") - if el.get_queued_delta_count() != 1: - push_error("Expected 1 queued delta, got %d" % el.get_queued_delta_count()) - return false - return true + assert_that(el.get_queued_delta_count()).is_equal(1) -# โ”€โ”€ AC-1: resolve_moral_queue increments flag โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_resolve_moral_queue_increments_flag() -> bool: +func test_resolve_moral_queue_increments_flag() -> void: var el: _EntityLifecycle = _new_lifecycle() var player: Entity = Entity.new("Player", 0, 0, 40, 12, 6) player.is_player = true @@ -180,14 +84,10 @@ func test_resolve_moral_queue_increments_flag() -> bool: el.apply_damage(null, enemy, 10) el.process_kill(player, enemy, true, "enemy_01", "Grunt") el.resolve_moral_queue() - if player.moral_flag != 1: - push_error("Expected moral_flag 1, got %d" % player.moral_flag) - return false - return true + assert_that(player.moral_flag).is_equal(1) -# โ”€โ”€ AC-2: MWT fires at exactly 3 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_mwt_fires_at_three() -> bool: +func test_mwt_fires_at_three() -> void: var el: _EntityLifecycle = _new_lifecycle() var player: Entity = Entity.new("Player", 0, 0, 40, 12, 6) player.is_player = true @@ -196,7 +96,7 @@ func test_mwt_fires_at_three() -> bool: var results := {"hit": false, "flag": -1} el.mwt_reached.connect( - func(flag: int, remaining: int) -> void: + func(flag: int, _remaining: int) -> void: results.hit = true results.flag = flag ) @@ -206,24 +106,17 @@ func test_mwt_fires_at_three() -> bool: el.process_kill(player, enemy, true, "enemy_01", "Grunt") el.resolve_moral_queue() - if not results.hit: - push_error("Expected mwt_reached signal at MWT=3") - return false - if results.flag != 3: - push_error("Expected flag 3, got %d" % results.flag) - return false - return true + assert_that(results.hit).is_true() + assert_that(results.flag).is_equal(3) -# โ”€โ”€ AC-2: MWT queues remaining deltas for next legal moment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_mwt_queues_remaining_deltas() -> bool: +func test_mwt_queues_remaining_deltas() -> void: var el: _EntityLifecycle = _new_lifecycle() var player: Entity = Entity.new("Player", 0, 0, 40, 12, 6) player.is_player = true player.moral_flag = 2 el.player_entity = player - ## Kill 3 enemies in one phase (AoE simulation) for i in range(3): var enemy: Entity = Entity.new("Enemy%d" % i, 0, 0, 10, 5, 3) el.apply_damage(null, enemy, 10) @@ -231,29 +124,15 @@ func test_mwt_queues_remaining_deltas() -> bool: el.resolve_moral_queue() - ## First MWT crossing should process exactly 1 delta (from 2โ†’3) - ## and leave the remaining 2 queued. - if player.moral_flag != 3: - push_error("Expected moral_flag 3 after first resolve, got %d" % player.moral_flag) - return false - if el.get_queued_delta_count() != 2: - push_error("Expected 2 remaining deltas, got %d" % el.get_queued_delta_count()) - return false + assert_that(player.moral_flag).is_equal(3) + assert_that(el.get_queued_delta_count()).is_equal(2) - ## Second resolve processes ALL remaining deltas (since no more MWT crossings) el.resolve_moral_queue() - if player.moral_flag != 5: - push_error("Expected moral_flag 5 after second resolve, got %d" % player.moral_flag) - return false - if el.get_queued_delta_count() != 0: - push_error("Expected 0 remaining deltas, got %d" % el.get_queued_delta_count()) - return false + assert_that(player.moral_flag).is_equal(5) + assert_that(el.get_queued_delta_count()).is_equal(0) - return true - -# โ”€โ”€ AC-1: Spare applies negative MORAL_DELTA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_spare_applies_negative_delta() -> bool: +func test_spare_applies_negative_delta() -> void: var el: _EntityLifecycle = _new_lifecycle() var player: Entity = Entity.new("Player", 0, 0, 40, 12, 6) player.is_player = true @@ -266,14 +145,10 @@ func test_spare_applies_negative_delta() -> bool: el.spare_entity(player, enemy) el.resolve_moral_queue() - if player.moral_flag != 1: - push_error("Expected moral_flag 1 after spare, got %d" % player.moral_flag) - return false - return true + assert_that(player.moral_flag).is_equal(1) -# โ”€โ”€ AC-3: Reset clears queue and timers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func test_reset_clears_queue_and_timers() -> bool: +func test_reset_clears_queue_and_timers() -> void: var el: _EntityLifecycle = _new_lifecycle() var enemy: Entity = Entity.new("TestEnemy", 0, 0, 10, 5, 3) el.stun(enemy, 3) @@ -282,12 +157,4 @@ func test_reset_clears_queue_and_timers() -> bool: el.reset_moral_queue() el.clear_timers() - if el.get_queued_delta_count() != 0: - push_error("Expected empty queue after reset") - return false - return true - - -# โ”€โ”€ Test Runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -func _initialize() -> void: - run_all() + assert_that(el.get_queued_delta_count()).is_equal(0) diff --git a/tests/test_localization_manager.gd b/tests/test_localization_manager.gd index 7bb7711..c5b5495 100644 --- a/tests/test_localization_manager.gd +++ b/tests/test_localization_manager.gd @@ -1,38 +1,8 @@ -extends SceneTree +class_name TestLocalizationManager +extends GdUnitTestSuite -## Test: LocalizationManager -## Verifies that translations are loaded and correctly applied. - -func _initialize() -> void: - # LocalizationManager is an autoload, but since we are running this script - # via 'godot -s', the SceneTree doesn't automatically load autoloads - # from project.godot for THIS script's tree. We need to manually add it - # to match the runtime environment as closely as possible. - var lm_script := load("res://scripts/autoload/localization_manager.gd") as GDScript - var lm: Node = lm_script.new() - lm.name = "LocalizationManager" - root.add_child(lm) - - # Give it a frame to initialize - await process_frame - - var success: bool = true - success = success and await test_translations_loaded() - success = success and await test_set_locale() - - if success: - print("PASSED: test_localization_manager.gd") - quit(0) - else: - print("FAILED: test_localization_manager.gd") - quit(1) - - -func test_translations_loaded() -> bool: - print(" - Verifying translations are loaded...") - - # Default locale should be "en" +func test_translations_loaded() -> void: TranslationServer.set_locale("en") var keys_to_test: Dictionary = { @@ -44,45 +14,24 @@ func test_translations_loaded() -> bool: for key: String in keys_to_test: var expected: String = keys_to_test[key] var actual: String = tr(key) - if actual != expected: - push_error("Expected tr('%s') to be '%s', but got '%s'" % [key, expected, actual]) - return false - - print(" [OK] English translations verified.") - return true + assert_that(actual).is_equal(expected) -func test_set_locale() -> bool: - print(" - Verifying locale switching...") - - var lm: Node = root.get_node("LocalizationManager") +func test_set_locale() -> void: + var lm_script := load("res://scripts/autoload/localization_manager.gd") as GDScript + var lm: Node = auto_free(lm_script.new()) + add_child(lm) - # Test German lm.set_locale("de") var key_de: String = "menu.title.continue" var expected_de: String = "Fortsetzen" var actual_de: String = tr(key_de) + assert_that(actual_de).is_equal(expected_de) - if actual_de != expected_de: - push_error( - "Expected tr('%s') to be '%s' (DE), but got '%s'" % [key_de, expected_de, actual_de] - ) - return false - - # Test French lm.set_locale("fr") var key_fr: String = "menu.title.quit" var expected_fr: String = "Quitter" var actual_fr: String = tr(key_fr) + assert_that(actual_fr).is_equal(expected_fr) - if actual_fr != expected_fr: - push_error( - "Expected tr('%s') to be '%s' (FR), but got '%s'" % [key_fr, expected_fr, actual_fr] - ) - return false - - # Reset to English lm.set_locale("en") - - print(" [OK] Locale switching verified.") - return true diff --git a/tests/test_localization_manager.gd.uid b/tests/test_localization_manager.gd.uid new file mode 100644 index 0000000..aacf40d --- /dev/null +++ b/tests/test_localization_manager.gd.uid @@ -0,0 +1 @@ +uid://4x0ryxgqovhm diff --git a/tests/test_remap_ui.gd b/tests/test_remap_ui.gd index 8d06851..eb4dddc 100644 --- a/tests/test_remap_ui.gd +++ b/tests/test_remap_ui.gd @@ -1,58 +1,35 @@ -extends SceneTree - - -func _initialize() -> void: - test_remap_logic() - quit() +class_name TestRemapUI +extends GdUnitTestSuite func test_remap_logic() -> void: - print("Running Remap Logic Tests...") + if FileAccess.file_exists("user://remap.save"): + DirAccess.remove_absolute("user://remap.save") - var script: GDScript = load("res://scripts/ui/remap_panel.gd") as GDScript - var remap_panel: Node = script.new() + var scene: PackedScene = load("res://scenes/ui/remap_panel.tscn") as PackedScene + var remap_panel: Node = auto_free(scene.instantiate()) + add_child(remap_panel) - # Test action list creation - if not InputMap.has_action("test_action"): - InputMap.add_action("test_action") + if InputMap.has_action("test_action"): + InputMap.erase_action("test_action") + InputMap.add_action("test_action") var key: InputEventKey = InputEventKey.new() key.keycode = KEY_F + key.physical_keycode = KEY_F InputMap.action_add_event("test_action", key) - var router: Node = Node.new() - router.name = "InputRouter" - router.set("current_device", 0) - root.add_child(router) - - var panel_vbox: VBoxContainer = VBoxContainer.new() - panel_vbox.name = "VBoxContainer" - var panel_scroll: ScrollContainer = ScrollContainer.new() - panel_scroll.name = "ScrollContainer" - var action_list: VBoxContainer = VBoxContainer.new() - action_list.name = "ActionList" - panel_scroll.add_child(action_list) - panel_vbox.add_child(panel_scroll) - remap_panel.add_child(panel_vbox) - - var toast: Label = Label.new() - toast.name = "ConflictToast" - remap_panel.add_child(toast) - - root.add_child(remap_panel) - - remap_panel.call("_ready") - print("Action list created") - - # Test conflict detection + var acts: Array[StringName] = [] + for a: StringName in InputMap.get_actions(): + if a == &"test_action": + acts.append(a) + print("Actions list: ", acts) + print("Events for test_action: ", InputMap.action_get_events("test_action")) var conflict: StringName = remap_panel.call("find_conflict", key, "other_action") as StringName - if conflict == "test_action": - print("Conflict detection passed") - else: - print("Conflict detection failed: ", conflict) + assert_that(conflict).is_equal(StringName("test_action")) - # Test remap var new_key: InputEventKey = InputEventKey.new() new_key.keycode = KEY_G + new_key.physical_keycode = KEY_G remap_panel.call("remap_action_to", "test_action", new_key) var events: Array[InputEvent] = InputMap.action_get_events("test_action") @@ -62,14 +39,6 @@ func test_remap_logic() -> void: found = true break - if found: - print("Remap logic passed") - else: - print("Remap logic failed") + assert_that(found).is_true() - # Test Reset remap_panel.call("_on_reset_pressed") - print("Reset to defaults called") - - print("Remap Logic Tests Completed") - remap_panel.queue_free() diff --git a/tests/test_run_determinism.gd b/tests/test_run_determinism.gd index b4eac17..158d337 100644 --- a/tests/test_run_determinism.gd +++ b/tests/test_run_determinism.gd @@ -1,154 +1,72 @@ -extends SceneTree -## Unit tests for _RunManager deterministic seeding and persistence. -## Run via `godot --headless --path . -s tests/test_run_determinism.gd`. - - -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_replay_code_roundtrip", - "test_deterministic_room_queue", - "test_save_load_persistence", - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL") - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - quit(1) - else: - quit(0) - - -func test_replay_code_roundtrip() -> bool: +class_name TestRunDeterminism +extends GdUnitTestSuite + + +func test_replay_code_roundtrip() -> void: var seeds: Array[int] = [12345, 0, -1, 0x7FFFFFFFFFFFFFFF, 0x123456789ABCDEF0] for s: int in seeds: var code: String = _RunManager.seed_to_replay_code(s) - if code.length() != 16: - push_error("Expected 16-char replay code, got %d for seed %d" % [code.length(), s]) - return false + assert_that(code.length()).is_equal(16) var decoded: int = _RunManager.replay_code_to_seed(code) - if decoded != s: - # Note: Godot's hex_to_int handles unsigned 64-bit hex. - # If s was negative (like -1), decoded should be the same bit pattern. - # In Godot, -1 is 0xFFFFFFFFFFFFFFFF. - if s == -1 and decoded == -1: - continue - push_error( - "Replay code roundtrip failed: expected %d, got %d (code: %s)" % [s, decoded, code] - ) - return false - return true - - -func test_deterministic_room_queue() -> bool: + if s == -1 and decoded == -1: + continue + assert_that(decoded).is_equal(s) + + +func test_deterministic_room_queue() -> void: var seed_val: int = 987654321 - var rm1: _RunManager = _RunManager.new() + var rm1: _RunManager = auto_free(_RunManager.new()) rm1.setup_state_machine() - root.add_child(rm1) + add_child(rm1) rm1.memory_state_loaded = true rm1.cmd_start_run(seed_val) - # Fast-forward to generate rooms for _i: int in range(10): rm1.update(0.02) var queue1: Array = rm1.room_queue.duplicate(true) - rm1.queue_free() - var rm2: _RunManager = _RunManager.new() + var rm2: _RunManager = auto_free(_RunManager.new()) rm2.setup_state_machine() - root.add_child(rm2) + add_child(rm2) rm2.memory_state_loaded = true rm2.cmd_start_run(seed_val) - # Fast-forward to generate rooms for _i: int in range(10): rm2.update(0.02) var queue2: Array = rm2.room_queue.duplicate(true) - rm2.queue_free() - if queue1.size() != queue2.size(): - push_error("Room queues have different sizes: %d vs %d" % [queue1.size(), queue2.size()]) - return false + assert_that(queue1.size()).is_equal(queue2.size()) for i: int in range(queue1.size()): var r1: Dictionary = queue1[i] as Dictionary var r2: Dictionary = queue2[i] as Dictionary - if ( - r1["topology_seed"] != r2["topology_seed"] - or r1["encounter_seed"] != r2["encounter_seed"] - ): - push_error( - ( - "Room %d seeds differ: T1=%d, T2=%d, E1=%d, E2=%d" - % [ - i, - r1["topology_seed"], - r2["topology_seed"], - r1["encounter_seed"], - r2["encounter_seed"] - ] - ) - ) - return false - - return true - - -func test_save_load_persistence() -> bool: - var rm: _RunManager = _RunManager.new() + assert_that(r1["topology_seed"]).is_equal(r2["topology_seed"]) + assert_that(r1["encounter_seed"]).is_equal(r2["encounter_seed"]) + + +func test_save_load_persistence() -> void: + var rm: _RunManager = auto_free(_RunManager.new()) rm.setup_state_machine() - root.add_child(rm) + add_child(rm) rm.memory_state_loaded = true rm.cmd_start_run(1337) - # Fast-forward to generate rooms for _i: int in range(10): rm.update(0.02) - # Advance a room rm.cmd_combat_resolved() for _i: int in range(10): rm.update(0.02) rm.cmd_next_room() var saved: Dictionary = rm.save_run_state() + assert_that(saved["seed"]).is_equal(1337) + assert_that(saved["room_index"]).is_equal(1) - if saved["seed"] != 1337: - push_error("Saved seed mismatch: expected 1337, got %d" % saved["seed"]) - return false - if saved["room_index"] != 1: - push_error("Saved room_index mismatch: expected 1, got %d" % saved["room_index"]) - return false - - var rm2: _RunManager = _RunManager.new() - root.add_child(rm2) + var rm2: _RunManager = auto_free(_RunManager.new()) + add_child(rm2) rm2.load_run_state(saved) - if rm2.run_seed != 1337: - push_error("Loaded seed mismatch: expected 1337, got %d" % rm2.run_seed) - return false - if rm2.room_index != 1: - push_error("Loaded room_index mismatch: expected 1, got %d" % rm2.room_index) - return false - if rm2.room_queue.size() != rm.room_queue.size(): - push_error("Loaded queue size mismatch") - return false - - rm.queue_free() - rm2.queue_free() - return true - - -func _initialize() -> void: - run_all() + assert_that(rm2.run_seed).is_equal(1337) + assert_that(rm2.room_index).is_equal(1) + assert_that(rm2.room_queue.size()).is_equal(rm.room_queue.size()) diff --git a/tests/test_runner.gd b/tests/test_runner.gd deleted file mode 100644 index 37dceec..0000000 --- a/tests/test_runner.gd +++ /dev/null @@ -1,8 +0,0 @@ -class_name TestRunner -extends SceneTree - - -func _initialize() -> void: - var test: Node = (preload("res://tests/test_entity_lifecycle.gd") as GDScript).new() - get_root().add_child(test) - # The test node will call get_tree().quit() when done. diff --git a/tests/test_state_machine.gd b/tests/test_state_machine.gd index 0be6c2c..5063f30 100644 --- a/tests/test_state_machine.gd +++ b/tests/test_state_machine.gd @@ -1,81 +1,42 @@ -extends Node -## Unit / integration tests for BaseStateMachine framework and _RunManager lifecycle. -## Run via Godot Editor test runner or `godot --headless --script tests/test_state_machine.gd`. -## -## Covers: -## AC-1: Explicit state enumeration, registration, default/error states -## AC-2: Guarded transitions (accept / reject) -## AC-3: Entry / exit actions invoked in correct order -## AC-4: Transition actions run between exit and entry -## AC-5: Frame-rate independent update accumulates state_time -## AC-6: Error state fallback on invalid transition target -## AC-7: _RunManager full lifecycle SANCTUM โ†’ ... โ†’ RUN_RESOLUTION โ†’ SANCTUM -## AC-8: _RunManager biome boundary detection -## AC-9: _RunManager config-driven defaults (fallback if game_config.json missing) - - -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_base_registration_and_default", - "test_base_valid_transition", - "test_base_guard_blocks_invalid", - "test_base_guard_allows_valid", - "test_base_entry_exit_order", - "test_base_transition_action_between_exit_entry", - "test_base_error_fallback", - "test_base_update_delta_accumulation", - "test_run_manager_starts_in_sanctum", - "test_run_manager_requires_memory_loaded", - "test_run_manager_full_lifecycle", - "test_run_manager_biome_boundary", - "test_run_manager_player_defeat", - "test_run_manager_final_encounter_won", - "test_run_manager_config_loaded", - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL (returned %s)" % str(ok)) - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - push_error("StateMachine test suite had failures.") - get_tree().quit(1) - else: - get_tree().quit(0) - - -# =========================================================================== -# BaseStateMachine Tests -# =========================================================================== - - -func test_base_registration_and_default() -> bool: +class_name TestStateMachine +extends GdUnitTestSuite + + +func _new_empty_state_machine() -> BaseStateMachine: + return auto_free(BaseStateMachine.new()) + + +func _new_run_manager() -> _RunManager: + var rm: _RunManager = auto_free(_RunManager.new()) + rm.setup_state_machine() + add_child(rm) + return rm + + +func _always_true_guard(_ctx: Dictionary) -> bool: + return true + + +func _always_false_guard(_ctx: Dictionary) -> bool: + return false + + +func _make_logger(log: Array[String], msg: String) -> Callable: + return func(_ctx: Dictionary) -> void: log.append(msg) + + +func test_base_registration_and_default() -> void: var sm: BaseStateMachine = _new_empty_state_machine() sm.register_state(0, &"A") sm.register_state(1, &"B") sm.set_default_state(0) sm.initialize() - if sm.current_state != 0: - push_error("Expected default state 0, got %d" % sm.current_state) - return false - if sm.get_current_state_name() != &"A": - push_error("Expected state name A") - return false - return true + assert_that(sm.current_state).is_equal(0) + assert_that(sm.get_current_state_name()).is_equal(&"A") -func test_base_valid_transition() -> bool: +func test_base_valid_transition() -> void: var sm: BaseStateMachine = _new_empty_state_machine() sm.register_state(0, &"A") sm.register_state(1, &"B") @@ -84,16 +45,11 @@ func test_base_valid_transition() -> bool: sm.initialize() var ok: bool = sm.transition_to(1) - if not ok: - push_error("Expected transition Aโ†’B to succeed") - return false - if sm.current_state != 1: - push_error("Expected state B after transition") - return false - return true + assert_that(ok).is_true() + assert_that(sm.current_state).is_equal(1) -func test_base_guard_blocks_invalid() -> bool: +func test_base_guard_blocks_invalid() -> void: var sm: BaseStateMachine = _new_empty_state_machine() sm.register_state(0, &"A") sm.register_state(1, &"B") @@ -102,16 +58,11 @@ func test_base_guard_blocks_invalid() -> bool: sm.initialize() var ok: bool = sm.transition_to(1) - if ok: - push_error("Expected guarded Aโ†’B to be blocked") - return false - if sm.current_state != 0: - push_error("Expected state to remain A after blocked transition") - return false - return true + assert_that(ok).is_false() + assert_that(sm.current_state).is_equal(0) -func test_base_guard_allows_valid() -> bool: +func test_base_guard_allows_valid() -> void: var sm: BaseStateMachine = _new_empty_state_machine() sm.register_state(0, &"A") sm.register_state(1, &"B") @@ -120,16 +71,11 @@ func test_base_guard_allows_valid() -> bool: sm.initialize() var ok: bool = sm.transition_to(1) - if not ok: - push_error("Expected guarded Aโ†’B to succeed") - return false - if sm.current_state != 1: - push_error("Expected state B after transition") - return false - return true + assert_that(ok).is_true() + assert_that(sm.current_state).is_equal(1) -func test_base_entry_exit_order() -> bool: +func test_base_entry_exit_order() -> void: var sm: BaseStateMachine = _new_empty_state_machine() var log_list: Array[String] = [] @@ -141,19 +87,12 @@ func test_base_entry_exit_order() -> bool: log_list.clear() sm.transition_to(1) - if log_list.size() != 2: - push_error("Expected 2 logged calls, got %d: %s" % [log_list.size(), str(log_list)]) - return false - if log_list[0] != "exit_A": - push_error("Expected exit_A first, got %s" % log_list[0]) - return false - if log_list[1] != "enter_B": - push_error("Expected enter_B second, got %s" % log_list[1]) - return false - return true + assert_that(log_list.size()).is_equal(2) + assert_that(log_list[0]).is_equal("exit_A") + assert_that(log_list[1]).is_equal("enter_B") -func test_base_transition_action_between_exit_entry() -> bool: +func test_base_transition_action_between_exit_entry() -> void: var sm: BaseStateMachine = _new_empty_state_machine() var log_list: Array[String] = [] @@ -165,22 +104,13 @@ func test_base_transition_action_between_exit_entry() -> bool: log_list.clear() sm.transition_to(1) - if log_list.size() != 3: - push_error("Expected 3 logged calls, got %d: %s" % [log_list.size(), str(log_list)]) - return false - if log_list[0] != "exit_A": - push_error("Expected exit_A first, got %s" % log_list[0]) - return false - if log_list[1] != "action_0_1": - push_error("Expected action_0_1 second, got %s" % log_list[1]) - return false - if log_list[2] != "enter_B": - push_error("Expected enter_B third, got %s" % log_list[2]) - return false - return true + assert_that(log_list.size()).is_equal(3) + assert_that(log_list[0]).is_equal("exit_A") + assert_that(log_list[1]).is_equal("action_0_1") + assert_that(log_list[2]).is_equal("enter_B") -func test_base_error_fallback() -> bool: +func test_base_error_fallback() -> void: var sm: BaseStateMachine = _new_empty_state_machine() sm.register_state(0, &"A") sm.register_state(99, &"ERROR") @@ -188,133 +118,73 @@ func test_base_error_fallback() -> bool: sm.set_error_state(99) sm.initialize() - # Force transition to unregistered state id 5 sm.force_state(5) - # Should have fallen back to ERROR (99) - if sm.current_state != 99: - push_error("Expected fallback to ERROR state 99, got %d" % sm.current_state) - return false - return true + assert_that(sm.current_state).is_equal(99) -func test_base_update_delta_accumulation() -> bool: +func test_base_update_delta_accumulation() -> void: var sm: BaseStateMachine = _new_empty_state_machine() sm.register_state(0, &"A", Callable(), Callable(), Callable()) sm.set_default_state(0) sm.initialize() - if sm.state_time != 0.0: - push_error("Expected initial state_time 0") - return false + assert_that(sm.state_time).is_equal(0.0) sm.update(0.016) - if not is_equal_approx(sm.state_time, 0.016): - push_error("Expected state_time 0.016 after one update, got %f" % sm.state_time) - return false + assert_that(is_equal_approx(sm.state_time, 0.016)).is_true() sm.update(0.033) - if not is_equal_approx(sm.state_time, 0.049): - push_error("Expected state_time 0.049 after two updates, got %f" % sm.state_time) - return false - return true + assert_that(is_equal_approx(sm.state_time, 0.049)).is_true() -# =========================================================================== -# _RunManager Lifecycle Tests -# =========================================================================== - - -func test_run_manager_starts_in_sanctum() -> bool: +func test_run_manager_starts_in_sanctum() -> void: var rm: _RunManager = _new_run_manager() - if rm.current_state != _RunManager.RunState.SANCTUM: - push_error("Expected initial state SANCTUM, got %d" % rm.current_state) - return false - if rm.get_current_state_name() != &"SANCTUM": - push_error("Expected state name SANCTUM") - return false - return true + assert_that(rm.current_state).is_equal(_RunManager.RunState.SANCTUM) + assert_that(rm.get_current_state_name()).is_equal(&"SANCTUM") -func test_run_manager_requires_memory_loaded() -> bool: +func test_run_manager_requires_memory_loaded() -> void: var rm: _RunManager = _new_run_manager() - # memory_state_loaded defaults to false; guard should block transition var ok: bool = rm.transition_to(_RunManager.RunState.BIOME_GENERATION) - if ok: - push_error("Expected start_run blocked when memory_state_loaded=false") - return false - if rm.current_state != _RunManager.RunState.SANCTUM: - push_error("Expected remain in SANCTUM") - return false - return true + assert_that(ok).is_false() + assert_that(rm.current_state).is_equal(_RunManager.RunState.SANCTUM) -func test_run_manager_full_lifecycle() -> bool: - # Use a tiny room count to keep the test fast +func test_run_manager_full_lifecycle() -> void: var rm: _RunManager = _new_run_manager() rm.biome_count = 1 rm.rooms_per_biome_min = 2 rm.rooms_per_biome_max = 2 - # 1. SANCTUM โ†’ BIOME_GENERATION (with memory loaded) rm.memory_state_loaded = true rm.cmd_start_run() - if rm.current_state != _RunManager.RunState.BIOME_GENERATION: - push_error( - "Expected BIOME_GENERATION after start_run, got %s" % rm.get_current_state_name() - ) - return false + assert_that(rm.current_state).is_equal(_RunManager.RunState.BIOME_GENERATION) - # Fast-forward biome generation timer for _i: int in range(10): rm.update(0.02) - if rm.current_state != _RunManager.RunState.ROOM: - push_error("Expected ROOM after topology_ready, got %s" % rm.get_current_state_name()) - return false - if rm.room_queue.size() != 2: - push_error("Expected 2 rooms, got %d" % rm.room_queue.size()) - return false - if rm.room_index != 0: - push_error("Expected room_index 0, got %d" % rm.room_index) - return false - - # 2. ROOM โ†’ MORAL_EVAL โ†’ ROOM (simulate combat in room 0) + assert_that(rm.current_state).is_equal(_RunManager.RunState.ROOM) + assert_that(rm.room_queue.size()).is_equal(2) + assert_that(rm.room_index).is_equal(0) + rm.cmd_combat_resolved() - if rm.current_state != _RunManager.RunState.MORAL_EVAL: - push_error("Expected MORAL_EVAL after combat_resolved") - return false + assert_that(rm.current_state).is_equal(_RunManager.RunState.MORAL_EVAL) - # Fast-forward moral eval timer for _i: int in range(10): rm.update(0.02) - if rm.current_state != _RunManager.RunState.ROOM: - push_error("Expected ROOM after moral eval auto-resolve") - return false + assert_that(rm.current_state).is_equal(_RunManager.RunState.ROOM) - # 3. ROOM โ†’ ROOM (next room, no biome boundary since single biome) rm.cmd_next_room() - if rm.current_state != _RunManager.RunState.ROOM: - push_error("Expected ROOM after next_room") - return false - if rm.room_index != 1: - push_error("Expected room_index 1, got %d" % rm.room_index) - return false - - # 4. Final room โ†’ RUN_RESOLUTION (run end because queue exhausted) + assert_that(rm.current_state).is_equal(_RunManager.RunState.ROOM) + assert_that(rm.room_index).is_equal(1) + rm.cmd_next_room() - if rm.current_state != _RunManager.RunState.RUN_RESOLUTION: - push_error("Expected RUN_RESOLUTION after final room, got %s" % rm.get_current_state_name()) - return false + assert_that(rm.current_state).is_equal(_RunManager.RunState.RUN_RESOLUTION) - # 5. RUN_RESOLUTION โ†’ SANCTUM rm.cmd_return_to_sanctum() - if rm.current_state != _RunManager.RunState.SANCTUM: - push_error("Expected SANCTUM after return_to_sanctum") - return false + assert_that(rm.current_state).is_equal(_RunManager.RunState.SANCTUM) - return true - -func test_run_manager_biome_boundary() -> bool: +func test_run_manager_biome_boundary() -> void: var rm: _RunManager = _new_run_manager() rm.biome_count = 2 rm.rooms_per_biome_min = 1 @@ -325,32 +195,18 @@ func test_run_manager_biome_boundary() -> bool: for _i: int in range(10): rm.update(0.02) - # room 0 (biome 0) -> next room is biome 1 => boundary - if rm.room_queue.size() != 2: - push_error("Expected 2 rooms for 2 biomes ร— 1") - return false + assert_that(rm.room_queue.size()).is_equal(2) rm.cmd_next_room() - if rm.current_state != _RunManager.RunState.BIOME_THRESHOLD: - push_error( - "Expected BIOME_THRESHOLD at biome boundary, got %s" % rm.get_current_state_name() - ) - return false + assert_that(rm.current_state).is_equal(_RunManager.RunState.BIOME_THRESHOLD) - # Fast-forward echo timer for _i: int in range(10): rm.update(0.02) - if rm.current_state != _RunManager.RunState.ROOM: - push_error("Expected ROOM after echo_triggered") - return false - if rm.room_index != 1: - push_error("Expected room_index 1 after boundary, got %d" % rm.room_index) - return false + assert_that(rm.current_state).is_equal(_RunManager.RunState.ROOM) + assert_that(rm.room_index).is_equal(1) - return true - -func test_run_manager_player_defeat() -> bool: +func test_run_manager_player_defeat() -> void: var rm: _RunManager = _new_run_manager() rm.biome_count = 1 rm.rooms_per_biome_min = 3 @@ -369,21 +225,13 @@ func test_run_manager_player_defeat() -> bool: for _i: int in range(10): rm.update(0.02) - # Player dies in first room rm.cmd_player_defeated() - if rm.current_state != _RunManager.RunState.RUN_RESOLUTION: - push_error("Expected RUN_RESOLUTION after player_defeated") - return false - if not results.received: - push_error("Expected run_ended signal") - return false - if results.value != &"DEFEAT": - push_error("Expected DEFEAT result, got %s" % results.value) - return false - return true + assert_that(rm.current_state).is_equal(_RunManager.RunState.RUN_RESOLUTION) + assert_that(results.received).is_true() + assert_that(results.value).is_equal(&"DEFEAT") -func test_run_manager_final_encounter_won() -> bool: +func test_run_manager_final_encounter_won() -> void: var rm: _RunManager = _new_run_manager() rm.biome_count = 1 rm.rooms_per_biome_min = 2 @@ -402,60 +250,13 @@ func test_run_manager_final_encounter_won() -> bool: for _i: int in range(10): rm.update(0.02) - # Win final encounter rm.cmd_final_encounter_won() - if rm.current_state != _RunManager.RunState.RUN_RESOLUTION: - push_error("Expected RUN_RESOLUTION after final_encounter_won") - return false - if not results.received: - push_error("Expected run_ended signal") - return false - if results.value != &"TRIUMPH": - push_error("Expected TRIUMPH result, got %s" % results.value) - return false - return true + assert_that(rm.current_state).is_equal(_RunManager.RunState.RUN_RESOLUTION) + assert_that(results.received).is_true() + assert_that(results.value).is_equal(&"TRIUMPH") -func test_run_manager_config_loaded() -> bool: +func test_run_manager_config_loaded() -> void: var rm: _RunManager = _new_run_manager() - # If game_config.json is present, values should be loaded from it. - # If missing, hard-coded defaults per ConfigLoader DEFAULTS apply. - if rm.biome_count == 0: - push_error("Expected biome_count > 0 after config load") - return false - if rm.rooms_per_biome_min > rm.rooms_per_biome_max: - push_error("rooms_per_biome_min must not exceed max") - return false - return true - - -# =========================================================================== -# Helpers -# =========================================================================== - - -func _new_empty_state_machine() -> BaseStateMachine: - var sm: BaseStateMachine = BaseStateMachine.new() - return sm - - -func _new_run_manager() -> _RunManager: - var rm: _RunManager = _RunManager.new() - rm.setup_state_machine() - return rm - - -func _always_true_guard(_ctx: Dictionary) -> bool: - return true - - -func _always_false_guard(_ctx: Dictionary) -> bool: - return false - - -func _make_logger(log: Array[String], msg: String) -> Callable: - return func(_ctx: Dictionary) -> void: log.append(msg) - - -func _ready() -> void: - run_all() + assert_that(rm.biome_count).is_greater(0) + assert_that(rm.rooms_per_biome_min <= rm.rooms_per_biome_max).is_true() diff --git a/tests/test_tile_data.gd b/tests/test_tile_data.gd index 3fe44d9..05df546 100644 --- a/tests/test_tile_data.gd +++ b/tests/test_tile_data.gd @@ -1,98 +1,39 @@ -extends Node +extends GdUnitTestSuite ## Unit tests for TacTileData -func test_is_blocked_default() -> bool: +func test_is_blocked_default() -> void: var tile := TacTileData.new() tile.recompute_flags() - if tile.is_blocked(): - push_error("Expected default tile to not be blocked") - return false - return true + assert_that(tile.is_blocked()).is_false() -func test_is_blocked_after_mutation() -> bool: +func test_is_blocked_after_mutation() -> void: var tile := TacTileData.new() tile.blocks_movement = true tile.recompute_flags() - if not tile.is_blocked(): - push_error("Expected mutated tile to be blocked") - return false - return true + assert_that(tile.is_blocked()).is_true() -func test_cover_defaults() -> bool: +func test_cover_defaults() -> void: var tile := TacTileData.new() tile.recompute_flags() - if tile.has_cover() or tile.is_light_cover() or tile.is_heavy_cover(): - push_error("Expected default tile to have no cover") - return false - return true + assert_that(tile.has_cover() or tile.is_light_cover() or tile.is_heavy_cover()).is_false() -func test_light_cover() -> bool: +func test_light_cover() -> void: var tile := TacTileData.new() tile.cover = TacTileData.CoverType.LIGHT tile.recompute_flags() - if not tile.has_cover(): - push_error("Expected tile to have cover") - return false - if not tile.is_light_cover(): - push_error("Expected tile to be light cover") - return false - if tile.is_heavy_cover(): - push_error("Expected tile not to be heavy cover") - return false - return true + assert_that(tile.has_cover()).is_true() + assert_that(tile.is_light_cover()).is_true() + assert_that(tile.is_heavy_cover()).is_false() -func test_heavy_cover() -> bool: +func test_heavy_cover() -> void: var tile := TacTileData.new() tile.cover = TacTileData.CoverType.HEAVY tile.recompute_flags() - if not tile.has_cover(): - push_error("Expected tile to have cover") - return false - if tile.is_light_cover(): - push_error("Expected tile not to be light cover") - return false - if not tile.is_heavy_cover(): - push_error("Expected tile to be heavy cover") - return false - return true - - -func _ready() -> void: - # Small delay to ensure all nodes are ready - await get_tree().process_frame - run_all() - - -func run_all() -> void: - var passed: int = 0 - var failed: int = 0 - var tests: Array[String] = [ - "test_is_blocked_default", - "test_is_blocked_after_mutation", - "test_cover_defaults", - "test_light_cover", - "test_heavy_cover", - ] - - for name: String in tests: - print("Running %s ..." % name) - var ok: Variant = call(name) - if ok is bool and ok: - passed += 1 - print(" PASS") - else: - failed += 1 - print(" FAIL (returned %s)" % str(ok)) - - print("") - print("Results: %d passed, %d failed out of %d" % [passed, failed, tests.size()]) - if failed > 0: - push_error("TacTileData test suite had failures.") - get_tree().quit(1) - else: - get_tree().quit(0) + assert_that(tile.has_cover()).is_true() + assert_that(tile.is_light_cover()).is_false() + assert_that(tile.is_heavy_cover()).is_true() diff --git a/tests/test_tile_data.gd.uid b/tests/test_tile_data.gd.uid new file mode 100644 index 0000000..39f6d57 --- /dev/null +++ b/tests/test_tile_data.gd.uid @@ -0,0 +1 @@ +uid://wox55gkbsjeh diff --git a/tests/test_ui_reflow.gd b/tests/test_ui_reflow.gd index ae32f1a..db83300 100644 --- a/tests/test_ui_reflow.gd +++ b/tests/test_ui_reflow.gd @@ -1,74 +1,30 @@ -extends Node - +extends GdUnitTestSuite ## Unit tests for SafeZoneManager and UI reflow logic (DON-196). -var _passed: int = 0 -var _failed: int = 0 - - -func _ready() -> void: - print("=== UI Reflow Test Suite (DON-196) ===") - - _test_aspect_ratio_breakpoints() - _test_safe_margins() - _test_notch_offset() - _test_portrait_detection() - - print("") - print("Results: %d passed, %d failed" % [_passed, _failed]) - if _failed > 0: - get_tree().quit(1) - else: - get_tree().quit(0) - - -func _assert(condition: bool, msg: String) -> void: - if condition: - _passed += 1 - else: - _failed += 1 - push_error("ASSERT FAILED: %s" % msg) - - -func _test_aspect_ratio_breakpoints() -> void: - print("\n[Test] Aspect ratio breakpoints") - - # We can't easily resize the viewport in headless mode and expect immediate signal response in a script run, - # but we can test the logic if we make it accessible or mock it. - # For now, we test the constants and the logic in SafeZoneManager if possible. - - _assert(SafeZoneManager.BREAKPOINT_SHRINK == 1.6, "Breakpoint SHRINK is 1.6") - _assert(SafeZoneManager.BREAKPOINT_EXPAND == 1.9, "Breakpoint EXPAND is 1.9") - # Manually trigger a check with a simulated size - # This is hard to do without modifying SafeZoneManager to accept a size for testing. - print(" breakpoints checked") +func test_aspect_ratio_breakpoints() -> void: + # Note: float comparison in exact types for deterministic rules + assert_that(SafeZoneManager.BREAKPOINT_SHRINK).is_equal(1.6) + assert_that(SafeZoneManager.BREAKPOINT_EXPAND).is_equal(1.9) -func _test_safe_margins() -> void: - print("\n[Test] Safe margins") - var margins := SafeZoneManager.get_safe_margins() - _assert(margins.has("left"), "Margins has left") - _assert(margins.has("right"), "Margins has right") - _assert(margins.has("top"), "Margins has top") - _assert(margins.has("bottom"), "Margins has bottom") - print(" margins checked") +func test_safe_margins() -> void: + var margins: Dictionary = SafeZoneManager.get_safe_margins() + assert_that(margins.has("left")).is_true() + assert_that(margins.has("right")).is_true() + assert_that(margins.has("top")).is_true() + assert_that(margins.has("bottom")).is_true() -func _test_notch_offset() -> void: - print("\n[Test] Notch offset") - var offset := SafeZoneManager.get_notch_offset() - _assert(offset is Vector2, "Notch offset is Vector2") - print(" notch offset checked") +func test_notch_offset() -> void: + var offset: Vector2 = SafeZoneManager.get_notch_offset() + assert_that(offset is Vector2).is_true() -func _test_portrait_detection() -> void: - print("\n[Test] Portrait detection") - # Headless default is usually landscape or square - var is_p := SafeZoneManager.is_portrait() - var size := get_viewport().get_visible_rect().size +func test_portrait_detection() -> void: + var is_p: bool = SafeZoneManager.is_portrait() + var size: Vector2 = get_viewport().get_visible_rect().size if size.y > size.x: - _assert(is_p == true, "Portrait detected correctly") + assert_that(is_p).is_true() else: - _assert(is_p == false, "Landscape detected correctly") - print(" portrait detection checked") + assert_that(is_p).is_false() diff --git a/tests/test_ui_systems.gd b/tests/test_ui_systems.gd index fdb01eb..17445fe 100644 --- a/tests/test_ui_systems.gd +++ b/tests/test_ui_systems.gd @@ -1,20 +1,17 @@ -extends Node - - -func _ready() -> void: - if not OS.has_feature("headless"): - test_toast_system() - test_modal_system() +extends GdUnitTestSuite +## Unit tests for UI systems func test_toast_system() -> void: - print("Testing Toast System...") + if OS.has_feature("headless"): + return ToastManager.show_toast("T_01_MESSAGE", ToastManager.ToastType.T_01) ToastManager.show_toast("T_02_MESSAGE", ToastManager.ToastType.T_02) func test_modal_system() -> void: - print("Testing Modal System...") + if OS.has_feature("headless"): + return var modal_scene: PackedScene = load("res://scenes/ui/modal.tscn") as PackedScene var modal: Node = modal_scene.instantiate() LayerManager.add_modal(modal) diff --git a/tests/test_visual_proxy.gd b/tests/test_visual_proxy.gd index 7c7a9f9..cf3d3f2 100644 --- a/tests/test_visual_proxy.gd +++ b/tests/test_visual_proxy.gd @@ -1,26 +1,18 @@ -extends SceneTree +extends GdUnitTestSuite -func _init() -> void: - print("--- Running EntityVisualProxy Tests ---") - +func test_visual_proxy() -> void: # Load classes manually to avoid parse errors in headless environment var EntityClass: GDScript = load("res://scripts/entities/entity.gd") var EntityVisualProxyClass: GDScript = load("res://scripts/visual/entity_visual_proxy.gd") # Mock GridRenderer since we are in a headless test environment without a full scene tree - var grid_renderer := Node2D.new() + var grid_renderer: Node2D = load("res://scripts/visual/grid_renderer.gd").new() grid_renderer.name = "GridRenderer" - # Standard 2:1 isometric formula as in grid_renderer.gd - var script := GDScript.new() - script.source_code = "extends Node2D\nfunc grid_to_world(x: int, y: int, elevation: int) -> Vector2:\n\treturn Vector2((float(x) - float(y)) * 32, (float(x) + float(y)) * 16 - float(elevation) * 16)\n" - script.reload() - grid_renderer.set_script(script) - - var root: Window = get_root() - var combat_room := Node2D.new() + + var combat_room: Node2D = auto_free(Node2D.new()) combat_room.name = "CombatRoom" - root.add_child(combat_room) + add_child(combat_room) combat_room.add_child(grid_renderer) # 1. Test Setup @@ -30,64 +22,57 @@ func _init() -> void: combat_room.add_child(proxy) # Wait for ready (one frame) - await process_frame + await get_tree().process_frame # 2. Verify Initial Sync var expected_pos: Vector2 = grid_renderer.call("grid_to_world", 1, 1, 0) var actual_pos: Vector2 = proxy.get("_target_position") - print("Expected pos: ", expected_pos, " Actual pos: ", actual_pos) - assert(actual_pos == expected_pos, "Initial target position mismatch") - print("[PASS] Initial sync correct") + assert_that(actual_pos).is_equal(expected_pos) # 3. Verify Position Change entity.set("x", 2) entity.set("y", 3) expected_pos = grid_renderer.call("grid_to_world", 2, 3, 0) actual_pos = proxy.get("_target_position") - print("Expected pos: ", expected_pos, " Actual pos: ", actual_pos) - assert(actual_pos == expected_pos, "Position change target mismatch") - print("[PASS] Position change correctly updates target") + assert_that(actual_pos).is_equal(expected_pos) # 4. Verify Elevation Change entity.set("elevation", 2) expected_pos = grid_renderer.call("grid_to_world", 2, 3, 2) actual_pos = proxy.get("_target_position") - print("Expected pos: ", expected_pos, " Actual pos: ", actual_pos) - assert(actual_pos == expected_pos, "Elevation change target mismatch") + assert_that(actual_pos).is_equal(expected_pos) + var shadow_sprite: Sprite2D = proxy.get("shadow_sprite") - assert(shadow_sprite.position.y == 32.0, "Shadow offset mismatch at elevation 2") + assert_that(shadow_sprite.position.y).is_equal(32.0) + var height_indicator: CanvasItem = proxy.get("height_indicator") - assert(height_indicator.visible == true, "Height indicator should be visible") - print("[PASS] Elevation change correctly updates target and visuals") + assert_that(height_indicator.visible).is_true() # 5. Verify Facing Change entity.set("facing_x", -1) var base_sprite: Sprite2D = proxy.get("base_sprite") - assert(base_sprite.flip_h == true, "Base sprite should be flipped for negative facing_x") + assert_that(base_sprite.flip_h).is_true() + entity.set("facing_x", 1) - assert(base_sprite.flip_h == false, "Base sprite should not be flipped for positive facing_x") - print("[PASS] Facing change correctly updates sprite flip") + assert_that(base_sprite.flip_h).is_false() # 6. Verify State Change entity.set("state", 2) # Entity.State.DYING = 2 - assert(proxy.modulate == Color(1.0, 0.4, 0.4), "Modulate mismatch for DYING state") - print("[PASS] State change correctly updates modulation") + assert_that(proxy.modulate).is_equal(Color(1.0, 0.4, 0.4)) # 7. Verify Damage Signal -> Apparition Effect var app_mock := Node2D.new() app_mock.name = "ApparitionRenderer" var app_script := GDScript.new() - app_script.source_code = "extends Node2D\nvar damage_triggered := false\nfunc trigger_damage_effect() -> void:\n\tdamage_triggered = true\n" + app_script.source_code = """ +extends Node2D +var damage_triggered := false +func trigger_damage_effect() -> void: + damage_triggered = true +""" app_script.reload() app_mock.set_script(app_script) proxy.add_child(app_mock) entity.set("hp", 5) # Damage from 10 to 5 - assert( - app_mock.get("damage_triggered") == true, - "Damage effect should have been triggered on ApparitionRenderer" - ) - print("[PASS] Damage signal correctly triggers apparition effect") - - print("--- All EntityVisualProxy Tests Passed ---") - quit() + assert_that(app_mock.get("damage_triggered")).is_true() diff --git a/tools/filter_godot_errors.py b/tools/filter_godot_errors.py new file mode 100644 index 0000000..e56eafa --- /dev/null +++ b/tools/filter_godot_errors.py @@ -0,0 +1,50 @@ +import sys +import re + +def filter_errors(log_file_path): + try: + with open(log_file_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except Exception as e: + print(f"Error reading {log_file_path}: {e}") + return False + + has_real_errors = False + skip_next = False + + for i in range(len(lines)): + if skip_next: + skip_next = False + continue + + line = lines[i] + + # Check if line is an error we care about + if re.search(r'SCRIPT ERROR:|Parse Error:|Compile Error|hides an autoload singleton|SHADER ERROR|ERROR: Failed to load script', line, re.IGNORECASE): + # The next line usually contains the location + next_line = lines[i+1] if i + 1 < len(lines) else "" + + # If the error or the location mentions the addons/gdUnit4 folder, we ignore it! + if 'addons/gdUnit4' in line or 'addons/gdUnit4' in next_line: + skip_next = True + continue + + # Otherwise, it's a real error in our codebase! + print(line.strip()) + if next_line.strip().startswith('at:'): + print(next_line.strip()) + skip_next = True + has_real_errors = True + + return has_real_errors + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 filter_godot_errors.py ") + sys.exit(1) + + has_errors = filter_errors(sys.argv[1]) + if has_errors: + sys.exit(1) + else: + sys.exit(0) diff --git a/tools/pre_push_check.sh b/tools/pre_push_check.sh index 560cf50..85f1227 100755 --- a/tools/pre_push_check.sh +++ b/tools/pre_push_check.sh @@ -42,29 +42,25 @@ echo "" echo "๐Ÿงน Step 2: Running GDScript Lint (Editor Scan)..." "$GODOT_BIN" --headless --editor --quit --path . 2>&1 | tee tools/godot_lint.log -# 3. In-Engine Math Validation -echo "" -echo "๐ŸŽฎ Step 3: Validating Deterministic Math (Godot)..." -"$GODOT_BIN" --headless --path . -s tests/test_deterministic_math.gd 2>&1 | tee tools/math_validation.log +# (Step 3 was removed since Godot standalone math validation was migrated to GdUnit4) # 4. Full Test Suite (NEW) echo "" -echo "๐Ÿงช Step 4: Running Full Test Suite..." -if [ -f tests/run_all_tests.sh ]; then - chmod +x tests/run_all_tests.sh - export GODOT_BIN +echo "๐Ÿงช Step 4: Running Full Test Suite via GdUnit4..." +if [ -f "addons/gdUnit4/bin/GdUnitCmdTool.gd" ]; then # Use || true to capture exit code without set -e killing script immediately - tests/run_all_tests.sh 2>&1 | tee tools/test_suite.log || TEST_EXIT_CODE=$? - TEST_EXIT_CODE=${TEST_EXIT_CODE:-0} - - if [ $TEST_EXIT_CODE -ne 0 ]; then + "$GODOT_BIN" --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a tests/ --ignoreHeadlessMode 2>&1 | tee tools/test_suite.log || TEST_EXIT_CODE=$? + + # GdUnit4 returns 100 for failures, 101 for warnings (like orphans), and 0 for pure success. + # We will treat 0 and 101 as passed for CI/Push checks, but 100 as failure. + if [ "$TEST_EXIT_CODE" = "100" ] || [ "$TEST_EXIT_CODE" = "1" ]; then echo "------------------------------------------------" echo "โŒ TEST SUITE FAILED! Check tools/test_suite.log" echo "------------------------------------------------" exit 1 fi else - echo "โš ๏ธ Test suite script not found at tests/run_all_tests.sh" + echo "โš ๏ธ GdUnit4 not found at addons/gdUnit4/bin/GdUnitCmdTool.gd" echo "Skipping test suite..." fi @@ -77,8 +73,8 @@ if grep -iE "SCRIPT ERROR|Parse Error|Compile Error|hides an autoload singleton| exit 1 fi -# Also check for general ERROR: but exclude common exit-leak false positives -if grep "ERROR:" tools/godot_lint.log tools/math_validation.log tools/test_suite.log | grep -vE "Resources still in use|ObjectDB instances leaked|Caller thread can't call this function in this node"; then +# Also check for general ERROR: but exclude common exit-leak false positives and intentional test errors +if grep "ERROR:" tools/godot_lint.log tools/test_suite.log 2>/dev/null | grep -ivE "resources still in use|objectdb instances leaked|caller thread can't call this function|statemachine: attempted to change to unregistered state"; then echo "------------------------------------------------" echo "โŒ CRITICAL ERRORS DETECTED! Check tools/*.log" echo "------------------------------------------------" diff --git a/ui/framework/focus_manager.gd b/ui/framework/focus_manager.gd index 1814067..6a8b89f 100644 --- a/ui/framework/focus_manager.gd +++ b/ui/framework/focus_manager.gd @@ -1,6 +1,6 @@ +# gdlint: disable=class-name class_name _FocusManager extends Node - ## FocusManager (DON-298) ## Handles keyboard/gamepad focus trapping for modals and provides a global focus ring. @@ -44,9 +44,13 @@ func pop_modal_focus() -> void: var state: Dictionary = _focus_stack.pop_back() for node: Variant in state.disabled_nodes.keys(): + if type_string(typeof(node)) == "Object" and not is_instance_valid(node): + continue + if node == null: + continue var control: Control = node as Control if is_instance_valid(control): - control.focus_mode = state.disabled_nodes[control] as Control.FocusMode + control.focus_mode = state.disabled_nodes[node] as Control.FocusMode func _create_focus_ring() -> void: