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() # We need finaly to send the test session close event because the current run is hard aborted. GdUnitSignals.instance().gdunit_event.emit(GdUnitSessionClose.new()) 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 ScriptEditorControls.save_all_open_script()