diff options
Diffstat (limited to 'addons/gdUnit4/src/core/runners')
9 files changed, 947 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd new file mode 100644 index 0000000..34dcfa3 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd @@ -0,0 +1,470 @@ +#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 <directory> +## +## # Run specific test suite with ignored tests +## runtest -a <directory> -i <testsuite:test_name> +## [/codeblock] + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +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 <directory|path of testsuite>", + "Adds the given test suite or directory to the execution pipeline.", + TYPE_STRING + ), + CmdOption.new( + "-i, --ignore", + "-i <testsuite_name|testsuite_name:test-name>", + "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 <directory>", + "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 <count>", + "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 <suite name>", "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) + if what == NOTIFICATION_PREDELETE: + prints("Finallize .. done") + + +func init_runner() -> void: + 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: + _state = EXIT + GdUnitTools.dispose_all() + await GdUnitMemoryObserver.gc_on_guarded_instances() + await super(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 <directory|path of testsuite> + runtest -a <directory> -i <path of testsuite|testsuite_name|testsuite_name:test_name> + """.dedent(), + Color.DARK_SALMON + ) + console_info( + "-- Options ---------------------------------------------------------------------------------------", + Color.DARK_SALMON + ) + for option in _cmd_options.default_options(): + descripe_option(option) + if show_advanced: + console_info( + "-- Advanced options --------------------------------------------------------------------------", + Color.DARK_SALMON + ) + for option in _cmd_options.advanced_options(): + descripe_option(option) + + +## Describes a single command line option.[br] +## [br] +## [param cmd_option] The option to describe. +func descripe_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 + + _test_cases = discover_tests() + 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) + 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..58e1061 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid @@ -0,0 +1 @@ +uid://dllu6e8jp1nuk diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd new file mode 100644 index 0000000..0fd8fc7 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd @@ -0,0 +1,90 @@ +@tool +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 + + +## Cleanup and quit the runner.[br] +## [br] +## [param code] The exit code to return. +func quit(code: int) -> void: + if code != RETURN_SUCCESS: + _state = EXIT + await GdUnitMemoryObserver.gc_on_guarded_instances() + await super.quit(code) + + +## 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..fd35f6b --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid @@ -0,0 +1 @@ +uid://cc77n13jrf5x6 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..aa729ff --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid @@ -0,0 +1 @@ +uid://dj37sbm0qh3sj diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd new file mode 100644 index 0000000..6551e08 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd @@ -0,0 +1,180 @@ +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. + +## 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_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 + +## Runner state machine +enum { + READY, + INIT, + RUN, + STOP, + EXIT +} + +func _init() -> void: + if OS.get_cmdline_args().size() == 1: + 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()) + 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 get_tree().process_frame + await get_tree().physics_frame + get_tree().quit(code) + + +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..cfd20db --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid @@ -0,0 +1 @@ +uid://wnlvwy04lmn6 |
