winit_test/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0 OR Zlib
2
3//! Run tests using a `winit` context.
4
5#![forbid(unsafe_code)]
6
7/// Re-exporting `winit` for the sake of convenience.
8pub use winit;
9
10/// The whole point.
11#[macro_export]
12macro_rules! main {
13    ($ty:ty => $($test:expr),*) => {
14        #[cfg(target_arch = "wasm32")]
15        $crate::__private::wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
16
17        #[cfg(not(target_os = "android"))]
18        #[cfg_attr(target_arch = "wasm32", $crate::__private::wasm_bindgen_test::wasm_bindgen_test)]
19        fn main() -> Result<(), Box<dyn std::error::Error>> {
20            const TESTS: &[$crate::__private::WinitBasedTest<$ty>] = &[$(
21                $crate::__winit_test_internal_collect_test!($test)
22            ),*];
23
24            $crate::__private::run(TESTS, ());
25            Ok(())
26        }
27
28        #[cfg(target_os = "android")]
29        #[no_mangle]
30        fn android_main(app: $crate::__private::Context) {
31            const TESTS: &[$crate::__private::WinitBasedTest<$ty>] = &[$(
32                $crate::__winit_test_internal_collect_test!($test)
33            ),*];
34
35            $crate::__private::run(TESTS, app);
36        }
37    };
38    ($($tt:tt)*) => {
39        $crate::main!(() => $($tt)*);
40    }
41}
42
43#[doc(hidden)]
44#[macro_export]
45macro_rules! __winit_test_internal_collect_test {
46    ($name:expr) => {
47        $crate::__private::WinitBasedTest {
48            name: stringify!($name),
49            function: $crate::__private::TestFunction::Oneoff($name),
50        }
51    };
52}
53
54#[doc(hidden)]
55// This part is semver-exempt.
56pub mod __private {
57    #[cfg(target_arch = "wasm32")]
58    pub use wasm_bindgen_test;
59
60    use winit::event_loop::EventLoopBuilder;
61    pub use winit::event_loop::EventLoopWindowTarget;
62
63    use owo_colors::OwoColorize;
64    use std::any::Any;
65    use std::panic::{catch_unwind, AssertUnwindSafe};
66    #[cfg(not(target_arch = "wasm32"))]
67    use std::time::Instant;
68    #[cfg(target_arch = "wasm32")]
69    use web_time::Instant;
70
71    #[cfg(target_os = "android")]
72    pub use winit::platform::android::{
73        activity::AndroidApp as Context, EventLoopBuilderExtAndroid,
74    };
75    #[cfg(target_arch = "wasm32")]
76    use winit::platform::web::EventLoopExtWebSys;
77    #[cfg(not(target_os = "android"))]
78    pub type Context = ();
79
80    struct State {
81        passed: i32,
82        panics: Vec<(&'static str, Box<dyn Any + Send>)>,
83        start: Instant,
84        run: bool,
85        code: i32,
86    }
87
88    /// Run a set of tests using a `winit` context.
89    pub fn run<T: 'static>(tests: &'static [WinitBasedTest<T>], _ctx: Context) {
90        // If we're on Miri, we can't run the tests.
91        if cfg!(miri) {
92            eprintln!();
93            eprintln!(
94                "{}: tests cannot be run under miri",
95                "warning(winit-test)".yellow().bold()
96            );
97            eprintln!("    = See this issue for more information: https://github.com/notgull/winit-test/issues/2");
98            eprintln!();
99            return;
100        }
101
102        // Create a new event loop and obtain a window target.
103        let mut builder = EventLoopBuilder::<T>::with_user_event();
104
105        // Install the Android event loop extension if necessary.
106        #[cfg(target_os = "android")]
107        {
108            builder.with_android_app(_ctx);
109        }
110
111        let event_loop = builder.build().expect("Failed to build event loop");
112
113        println!("\nRunning {} tests...", tests.len());
114        let mut state = State {
115            passed: 0,
116            panics: vec![],
117            start: Instant::now(),
118            run: false,
119            code: 0,
120        };
121
122        // Run the tests.
123        #[cfg(not(target_arch = "wasm32"))]
124        event_loop
125            .run(move |_, elwt| {
126                run_internal(tests, &mut state, elwt);
127            })
128            .expect("Event loop failed to run");
129        #[cfg(target_arch = "wasm32")]
130        event_loop.spawn(move |_, elwt| {
131            run_internal(tests, &mut state, elwt);
132        });
133    }
134
135    /// Run a set of tests using a `winit` context.
136    fn run_internal<T: 'static>(
137        tests: &'static [WinitBasedTest<T>],
138        state: &mut State,
139        elwt: &EventLoopWindowTarget<T>,
140    ) {
141        if state.run {
142            elwt.exit();
143            return;
144        }
145        state.run = true;
146
147        for test in tests {
148            print!("test {} ... ", test.name);
149
150            match test.function {
151                TestFunction::Oneoff(f) => match catch_unwind(AssertUnwindSafe(move || f(elwt))) {
152                    Ok(()) => {
153                        println!("{}", "ok".green());
154                        state.passed += 1;
155                    }
156
157                    Err(e) => {
158                        println!("{}", "FAILED".red());
159                        state.panics.push((test.name, e));
160                    }
161                },
162            }
163        }
164
165        let failures = state.panics.len();
166        println!();
167        if !state.panics.is_empty() {
168            println!("failures:\n");
169            for (name, e) in state.panics.drain(..) {
170                println!("---- {} panic ----", name);
171
172                if let Some(s) = e.downcast_ref::<&'static str>() {
173                    println!("{}", s.red());
174                } else if let Some(s) = e.downcast_ref::<String>() {
175                    println!("{}", s.red());
176                } else {
177                    println!("{}", "unknown panic type".red());
178                }
179
180                println!();
181            }
182
183            print!("test result: {}", "FAILED".red());
184        } else {
185            print!("test result: {}", "ok".green());
186        }
187
188        let elapsed = state.start.elapsed();
189        println!(
190            ". {} passed; {} failed; finished in {:?}",
191            state.passed, failures, elapsed
192        );
193
194        state.code = if failures == 0 { 0 } else { 1 };
195        elwt.exit();
196    }
197
198    pub struct WinitBasedTest<T: 'static> {
199        pub name: &'static str,
200        pub function: TestFunction<T>,
201    }
202
203    pub enum TestFunction<T: 'static> {
204        Oneoff(fn(&EventLoopWindowTarget<T>)),
205    }
206}