godot_testability_runtime/
test_main.rs

1//! Macro for generating the test main function with minimal boilerplate.
2
3/// Generates a main function for running Godot tests with cargo test.
4///
5/// This macro creates all the necessary boilerplate for:
6/// - Setting up libtest_mimic for proper test output
7/// - Running tests on the main thread (required for macOS)
8/// - Initializing Godot runtime only once
9/// - Supporting cargo test filtering and options
10///
11/// # Usage
12///
13/// In your test file (with `harness = false` in Cargo.toml):
14///
15/// ```rust,ignore
16/// use godot::prelude::*;
17/// use godot_testability_runtime::prelude::*;
18/// use godot_testability_runtime::godot_test_main;
19///
20/// fn test_vectors(scene_tree: &mut Gd<SceneTree>) -> TestResult<()> {
21///     let v = Vector2::new(1.0, 2.0);
22///     assert_eq!(v.x, 1.0);
23///     Ok(())
24/// }
25///
26/// fn test_strings(scene_tree: &mut Gd<SceneTree>) -> TestResult<()> {
27///     let s = GString::from("test");
28///     assert!(!s.is_empty());
29///     Ok(())
30/// }
31///
32/// // Generate the main function with your tests
33/// godot_test_main! {
34///     test_vectors,
35///     test_strings,
36/// }
37/// ```
38#[macro_export]
39macro_rules! godot_test_main {
40    ($($test_name:ident),* $(,)?) => {
41        fn main() {
42            use $crate::runtime::{GodotRuntime, RuntimeConfig, UserCallbacks};
43            use $crate::prelude::*;
44            use $crate::__private::libtest_mimic::{Arguments, Trial};
45            use ::std::sync::{Arc, Mutex};
46
47            // Parse command line arguments
48            let mut args = Arguments::from_args();
49
50            // Force single-threaded execution (required for Godot on macOS)
51            args.test_threads = Some(1);
52
53            // Collect all test functions
54            let tests: Vec<(&str, fn(&mut ::godot::prelude::Gd<::godot::prelude::SceneTree>) -> TestResult<()>)> = vec![
55                $((stringify!($test_name), $test_name),)*
56            ];
57
58            if tests.is_empty() {
59                eprintln!("No tests defined!");
60                std::process::exit(1);
61            }
62
63            // Storage for test results
64            let test_results = Arc::new(Mutex::new(Vec::new()));
65            let test_results_clone = test_results.clone();
66
67            // Filter tests based on command line arguments
68            let filter = args.filter.as_ref().map(|s| s.as_str());
69            let filtered_tests: Vec<_> = tests
70                .into_iter()
71                .filter(|(name, _)| filter.map_or(true, |f| name.contains(f)))
72                .collect();
73
74            if filtered_tests.is_empty() {
75                println!("No tests to run with filter: {:?}", filter);
76                std::process::exit(0);
77            }
78
79            // Create callbacks for godot-rust integration
80            let callbacks = UserCallbacks {
81                initialize_ffi: |get_proc_addr, library| {
82                    // Call godot-rust's initialization
83                    unsafe {
84                        type GetProcAddrFn = unsafe extern "C" fn(*const i8) -> Option<unsafe extern "C" fn()>;
85                        let get_proc_addr_fn = std::mem::transmute::<*const std::ffi::c_void, GetProcAddrFn>(get_proc_addr);
86                        let library_ptr = library as *mut ::godot::sys::__GdextClassLibrary;
87                        let config = ::godot::sys::GdextConfig::new(false);
88                        ::godot::sys::initialize(Some(get_proc_addr_fn), library_ptr, config);
89                    }
90                    Ok(())
91                },
92                load_class_method_table: |level| {
93                    // Map u32 back to godot's InitLevel
94                    let init_level = match level {
95                        0 => ::godot::init::InitLevel::Core,
96                        1 => ::godot::init::InitLevel::Servers,
97                        2 => ::godot::init::InitLevel::Scene,
98                        3 => ::godot::init::InitLevel::Editor,
99                        _ => return,
100                    };
101                    unsafe {
102                        ::godot::sys::load_class_method_table(init_level);
103                    }
104                },
105            };
106
107            // Run all tests in a single Godot runtime
108            let config = RuntimeConfig {
109                headless: true,
110                verbose: false,
111                custom_args: vec![],
112            };
113
114            let runtime_result = GodotRuntime::run_godot(config, callbacks, move |scene_tree_ptr| {
115                // Convert raw pointer to Gd<SceneTree>
116                let mut scene_tree = unsafe {
117                    let obj_ptr = scene_tree_ptr as ::godot::sys::GDExtensionObjectPtr;
118                    ::godot::prelude::Gd::<::godot::prelude::SceneTree>::from_sys_init_opt(|ptr| {
119                        *(ptr as *mut ::godot::sys::GDExtensionObjectPtr) = obj_ptr;
120                    })
121                    .expect("Failed to create SceneTree from pointer")
122                };
123
124                println!("\n🧪 Running {} Godot tests...\n", filtered_tests.len());
125
126                for (name, test_fn) in &filtered_tests {
127                    print!("test {} ... ", name);
128
129                    match test_fn(&mut scene_tree) {
130                        Ok(()) => {
131                            println!("ok");
132                            test_results_clone
133                                .lock()
134                                .unwrap()
135                                .push((name.to_string(), Ok(())));
136                        }
137                        Err(e) => {
138                            println!("FAILED");
139                            println!("  Error: {}", e);
140                            test_results_clone
141                                .lock()
142                                .unwrap()
143                                .push((name.to_string(), Err(format!("{}", e))));
144                        }
145                    }
146                }
147
148                println!();
149                scene_tree.quit();
150                Ok(())
151            });
152
153            // Handle runtime errors
154            if let Err(e) = runtime_result {
155                eprintln!("Runtime error: {}", e);
156                std::process::exit(1);
157            }
158
159            // Create trials from results for libtest_mimic reporting
160            let results = test_results.lock().unwrap().clone();
161            let trials: Vec<Trial> = results
162                .into_iter()
163                .map(|(name, result)| {
164                    Trial::test(name.clone(), move || {
165                        result.clone().map_err(|e| $crate::__private::libtest_mimic::Failed::from(e))
166                    })
167                })
168                .collect();
169
170            // Run the trials and exit
171            $crate::__private::libtest_mimic::run(&args, trials).exit();
172        }
173    };
174}