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}