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))