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