godot-testability-runtime 0.1.2

Embedded Godot runtime for comprehensive Rust testing
Documentation
//! Macro for generating the test main function with minimal boilerplate.

/// Generates a main function for running Godot tests with cargo test.
///
/// This macro creates all the necessary boilerplate for:
/// - Setting up libtest_mimic for proper test output
/// - Running tests on the main thread (required for macOS)
/// - Initializing Godot runtime only once
/// - Supporting cargo test filtering and options
///
/// # Usage
///
/// In your test file (with `harness = false` in Cargo.toml):
///
/// ```rust,ignore
/// use godot::prelude::*;
/// use godot_testability_runtime::prelude::*;
/// use godot_testability_runtime::godot_test_main;
///
/// fn test_vectors(scene_tree: &mut Gd<SceneTree>) -> TestResult<()> {
///     let v = Vector2::new(1.0, 2.0);
///     assert_eq!(v.x, 1.0);
///     Ok(())
/// }
///
/// fn test_strings(scene_tree: &mut Gd<SceneTree>) -> TestResult<()> {
///     let s = GString::from("test");
///     assert!(!s.is_empty());
///     Ok(())
/// }
///
/// // Generate the main function with your tests
/// godot_test_main! {
///     test_vectors,
///     test_strings,
/// }
/// ```
#[macro_export]
macro_rules! godot_test_main {
    ($($test_name:ident),* $(,)?) => {
        fn main() {
            use $crate::runtime::{GodotRuntime, RuntimeConfig, UserCallbacks};
            use $crate::prelude::*;
            use $crate::__private::libtest_mimic::{Arguments, Trial};
            use ::std::sync::{Arc, Mutex};

            // Parse command line arguments
            let mut args = Arguments::from_args();

            // Force single-threaded execution (required for Godot on macOS)
            args.test_threads = Some(1);

            // Collect all test functions
            let tests: Vec<(&str, fn(&mut ::godot::prelude::Gd<::godot::prelude::SceneTree>) -> TestResult<()>)> = vec![
                $((stringify!($test_name), $test_name),)*
            ];

            if tests.is_empty() {
                eprintln!("No tests defined!");
                std::process::exit(1);
            }

            // Storage for test results
            let test_results = Arc::new(Mutex::new(Vec::new()));
            let test_results_clone = test_results.clone();

            // Filter tests based on command line arguments
            let filter = args.filter.as_ref().map(|s| s.as_str());
            let filtered_tests: Vec<_> = tests
                .into_iter()
                .filter(|(name, _)| filter.map_or(true, |f| name.contains(f)))
                .collect();

            if filtered_tests.is_empty() {
                println!("No tests to run with filter: {:?}", filter);
                std::process::exit(0);
            }

            // Create callbacks for godot-rust integration
            let callbacks = UserCallbacks {
                initialize_ffi: |get_proc_addr, library| {
                    // Call godot-rust's initialization
                    unsafe {
                        type GetProcAddrFn = unsafe extern "C" fn(*const i8) -> Option<unsafe extern "C" fn()>;
                        let get_proc_addr_fn = std::mem::transmute::<*const std::ffi::c_void, GetProcAddrFn>(get_proc_addr);
                        let library_ptr = library as *mut ::godot::sys::__GdextClassLibrary;
                        let config = ::godot::sys::GdextConfig::new(false);
                        ::godot::sys::initialize(Some(get_proc_addr_fn), library_ptr, config);
                    }
                    Ok(())
                },
                load_class_method_table: |level| {
                    // Map u32 back to godot's InitLevel
                    let init_level = match level {
                        0 => ::godot::init::InitLevel::Core,
                        1 => ::godot::init::InitLevel::Servers,
                        2 => ::godot::init::InitLevel::Scene,
                        3 => ::godot::init::InitLevel::Editor,
                        _ => return,
                    };
                    unsafe {
                        ::godot::sys::load_class_method_table(init_level);
                    }
                },
            };

            // Run all tests in a single Godot runtime
            let config = RuntimeConfig {
                headless: true,
                verbose: false,
                custom_args: vec![],
            };

            let runtime_result = GodotRuntime::run_godot(config, callbacks, move |scene_tree_ptr| {
                // Convert raw pointer to Gd<SceneTree>
                let mut scene_tree = unsafe {
                    let obj_ptr = scene_tree_ptr as ::godot::sys::GDExtensionObjectPtr;
                    ::godot::prelude::Gd::<::godot::prelude::SceneTree>::from_sys_init_opt(|ptr| {
                        *(ptr as *mut ::godot::sys::GDExtensionObjectPtr) = obj_ptr;
                    })
                    .expect("Failed to create SceneTree from pointer")
                };

                println!("\n🧪 Running {} Godot tests...\n", filtered_tests.len());

                for (name, test_fn) in &filtered_tests {
                    print!("test {} ... ", name);

                    match test_fn(&mut scene_tree) {
                        Ok(()) => {
                            println!("ok");
                            test_results_clone
                                .lock()
                                .unwrap()
                                .push((name.to_string(), Ok(())));
                        }
                        Err(e) => {
                            println!("FAILED");
                            println!("  Error: {}", e);
                            test_results_clone
                                .lock()
                                .unwrap()
                                .push((name.to_string(), Err(format!("{}", e))));
                        }
                    }
                }

                println!();
                scene_tree.quit();
                Ok(())
            });

            // Handle runtime errors
            if let Err(e) = runtime_result {
                eprintln!("Runtime error: {}", e);
                std::process::exit(1);
            }

            // Create trials from results for libtest_mimic reporting
            let results = test_results.lock().unwrap().clone();
            let trials: Vec<Trial> = results
                .into_iter()
                .map(|(name, result)| {
                    Trial::test(name.clone(), move || {
                        result.clone().map_err(|e| $crate::__private::libtest_mimic::Failed::from(e))
                    })
                })
                .collect();

            // Run the trials and exit
            $crate::__private::libtest_mimic::run(&args, trials).exit();
        }
    };
}