godot_testability_runtime/
runtime.rs

1//! Godot runtime management for embedded testing.
2//!
3//! This module provides utilities for managing a Godot runtime instance
4//! within the test environment. It's inspired by the SwiftGodot approach
5//! but adapted for Rust and the current godot-rust ecosystem.
6
7use crate::error::{TestError, TestResult};
8use once_cell::sync::Lazy;
9use parking_lot::Mutex;
10use std::sync::Arc;
11use tracing::{error, info};
12
13// Type aliases for FFI pointers
14type GDExtensionInterfaceGetProcAddress = *const std::ffi::c_void;
15type GDExtensionClassLibraryPtr = *mut std::ffi::c_void;
16type SceneTreePtr = *mut std::ffi::c_void;
17
18// Global callbacks provided by the user
19static USER_CALLBACKS: Mutex<Option<UserCallbacks>> = Mutex::new(None);
20
21// Global scene callback to be executed when SceneTree is ready
22#[allow(clippy::type_complexity)]
23static SCENE_CALLBACK: Mutex<Option<Box<dyn FnOnce(SceneTreePtr) -> TestResult<()> + Send>>> =
24    Mutex::new(None);
25
26/// Callbacks that users must provide to integrate with their godot-rust version
27pub struct UserCallbacks {
28    /// Initialize godot-rust FFI
29    pub initialize_ffi:
30        fn(GDExtensionInterfaceGetProcAddress, GDExtensionClassLibraryPtr) -> Result<(), String>,
31    /// Load class method table for given init level
32    pub load_class_method_table: fn(u32),
33}
34
35/// Global state for the embedded Godot runtime.
36static RUNTIME_STATE: Lazy<Arc<Mutex<RuntimeState>>> =
37    Lazy::new(|| Arc::new(Mutex::new(RuntimeState::new())));
38
39/// Internal state of the Godot runtime.
40#[derive(Debug)]
41struct RuntimeState {
42    initialized: bool,
43    running: bool,
44}
45
46impl RuntimeState {
47    fn new() -> Self {
48        Self {
49            initialized: false,
50            running: false,
51        }
52    }
53}
54
55/// Configuration options for the Godot runtime.
56#[derive(Debug, Clone)]
57pub struct RuntimeConfig {
58    /// Run Godot in headless mode (no visual output).
59    pub headless: bool,
60    /// Enable verbose logging from Godot.
61    pub verbose: bool,
62    /// Custom command line arguments to pass to Godot.
63    pub custom_args: Vec<String>,
64}
65
66impl Default for RuntimeConfig {
67    fn default() -> Self {
68        Self {
69            headless: true,
70            verbose: false,
71            custom_args: Vec::new(),
72        }
73    }
74}
75
76/// Manager for the embedded Godot runtime.
77///
78/// This provides a safe interface for initializing, managing, and shutting down
79/// a Godot runtime instance for testing purposes. The runtime is designed to be
80/// lightweight and suitable for automated testing environments.
81pub struct GodotRuntime;
82
83#[cfg(feature = "embedded_runtime")]
84impl GodotRuntime {
85    /// Check if the Godot runtime is currently initialized.
86    pub fn is_initialized() -> bool {
87        RUNTIME_STATE.lock().initialized
88    }
89
90    /// Check if the Godot runtime is currently running.
91    pub fn is_running() -> bool {
92        RUNTIME_STATE.lock().running
93    }
94
95    /// Shut down the Godot runtime.
96    ///
97    /// This resets the runtime state. Actual cleanup is handled by run_godot.
98    pub fn shutdown() -> TestResult<()> {
99        let mut state = RUNTIME_STATE.lock();
100        if !state.initialized {
101            return Ok(());
102        }
103
104        info!("Shutting down Godot runtime");
105        state.running = false;
106        state.initialized = false;
107        Ok(())
108    }
109
110    /// Run Godot with SwiftGodot-style initialization.
111    ///
112    /// The load_scene callback receives a raw SceneTree pointer.
113    /// Users are responsible for converting this to their Godot type.
114    pub fn run_godot<F>(
115        _config: RuntimeConfig,
116        callbacks: UserCallbacks,
117        load_scene: F,
118    ) -> TestResult<i32>
119    where
120        F: FnOnce(SceneTreePtr) -> TestResult<()> + Send + 'static,
121    {
122        use crate::ffi::{
123            libgodot_gdextension_bind, GDExtensionClassLibraryPtr, GDExtensionInitialization,
124            GDExtensionInitializationLevel, GDExtensionInterfaceGetProcAddress,
125        };
126        use std::ffi::c_void;
127
128        info!("Starting Godot runtime with SwiftGodot-style initialization");
129
130        // Store callbacks globally
131        {
132            USER_CALLBACKS.lock().replace(callbacks);
133            SCENE_CALLBACK.lock().replace(Box::new(load_scene));
134        }
135
136        extern "C" fn initialization_callback(
137            get_proc_addr: Option<GDExtensionInterfaceGetProcAddress>,
138            library: GDExtensionClassLibraryPtr,
139            r_initialization: *mut GDExtensionInitialization,
140        ) -> i32 {
141            if get_proc_addr.is_none() || library.is_null() {
142                return 0;
143            }
144
145            unsafe {
146                if !r_initialization.is_null() {
147                    (*r_initialization).minimum_initialization_level =
148                        GDExtensionInitializationLevel::Core;
149                    (*r_initialization).userdata = library;
150                    (*r_initialization).initialize = Some(godot_rust_bridge_initialize);
151                    (*r_initialization).deinitialize = Some(godot_rust_bridge_deinitialize);
152                }
153
154                // Initialize godot-rust FFI using user's callback
155                if let Some(callbacks) = USER_CALLBACKS.lock().as_ref() {
156                    if let Some(get_proc_address_fn) = get_proc_addr {
157                        if let Err(e) = (callbacks.initialize_ffi)(
158                            get_proc_address_fn as *const c_void,
159                            library,
160                        ) {
161                            error!("Failed to initialize godot-rust FFI: {}", e);
162                            return 0;
163                        }
164                    }
165                }
166            }
167            1
168        }
169
170        extern "C" fn scene_callback(scene_tree_ptr: *mut c_void) {
171            info!("Scene tree ready - Godot engine available");
172            if !scene_tree_ptr.is_null() {
173                if let Some(callback) = SCENE_CALLBACK.lock().take() {
174                    info!("Executing test callback with SceneTree pointer");
175                    match callback(scene_tree_ptr) {
176                        Ok(()) => {
177                            info!("Test callback executed successfully");
178                        }
179                        Err(e) => {
180                            error!("Test callback failed: {:?}", e);
181                        }
182                    }
183                } else {
184                    info!("No test callback to execute");
185                }
186            } else {
187                error!("Scene tree pointer is null!");
188            }
189        }
190
191        unsafe {
192            libgodot_gdextension_bind(initialization_callback, Some(scene_callback));
193        }
194
195        std::env::set_var("__CFBundleIdentifier", "GodotBevyKit");
196
197        let args = vec![
198            "GodotBevyKit".to_string(),
199            "--headless".to_string(),
200            "--verbose".to_string(),
201            "--quit".to_string(),
202        ];
203
204        let mut runtime = crate::ffi::LibgodotRuntime::new();
205        runtime
206            .initialize()
207            .map_err(TestError::RuntimeInitialization)?;
208
209        info!("Starting godot_main");
210        let result = runtime
211            .run_main(&args)
212            .map_err(TestError::RuntimeInitialization)?;
213
214        info!("Godot main loop finished with exit code: {}", result);
215        Ok(result)
216    }
217}
218
219extern "C" fn godot_rust_bridge_initialize(
220    _userdata: *mut std::ffi::c_void,
221    level: crate::ffi::GDExtensionInitializationLevel,
222) {
223    info!("Godot-Rust bridge initialize (level: {:?})", level);
224
225    // Map to u32 for the user callback
226    let init_level = match level {
227        crate::ffi::GDExtensionInitializationLevel::Core => 0,
228        crate::ffi::GDExtensionInitializationLevel::Servers => 1,
229        crate::ffi::GDExtensionInitializationLevel::Scene => 2,
230        crate::ffi::GDExtensionInitializationLevel::Editor => 3,
231        _ => return,
232    };
233
234    // Call user's load_class_method_table
235    if let Some(callbacks) = USER_CALLBACKS.lock().as_ref() {
236        (callbacks.load_class_method_table)(init_level);
237    }
238}
239
240extern "C" fn godot_rust_bridge_deinitialize(
241    _userdata: *mut std::ffi::c_void,
242    level: crate::ffi::GDExtensionInitializationLevel,
243) {
244    info!("Godot-Rust bridge deinitialize (level: {:?})", level);
245}