Skip to main content

bevy_test_harness/
lib.rs

1mod app_ext;
2mod commands_ext;
3mod data;
4mod log;
5mod util;
6
7pub mod prelude {
8    pub use super::app_ext::*;
9    pub use super::commands_ext::*;
10    pub use super::data::*;
11    pub use super::log::*;
12    pub use super::*;
13    pub(crate) use bevy::prelude::*;
14}
15use bevy::{
16    app::ScheduleRunnerPlugin,
17    diagnostic::FrameCountPlugin,
18    log::{DEFAULT_FILTER, LogPlugin},
19    state::app::StatesPlugin,
20    time::TimePlugin,
21};
22
23use crate::prelude::*;
24
25/// Test runner timeout in seconds
26#[derive(Resource, Deref, PartialEq)]
27pub struct TestRunnerTimeout(pub f32);
28impl Default for TestRunnerTimeout {
29    fn default() -> Self {
30        Self(5.)
31    }
32}
33
34/// Should the test runner log step counts?
35#[derive(Resource, Deref, PartialEq, Eq)]
36pub struct LogTestSteps(pub bool);
37impl Default for LogTestSteps {
38    fn default() -> Self {
39        Self(true)
40    }
41}
42
43#[derive(Debug)]
44pub struct TestRunnerPlugin {
45    pub log_level: bevy::log::Level,
46    pub log_filter: String,
47}
48impl Default for TestRunnerPlugin {
49    fn default() -> Self {
50        Self {
51            log_level: bevy::log::Level::TRACE,
52            log_filter: DEFAULT_FILTER.to_string(),
53        }
54    }
55}
56impl Plugin for TestRunnerPlugin {
57    fn build(&self, app: &mut App) {
58        app.add_plugins((
59            TaskPoolPlugin::default(),
60            FrameCountPlugin,
61            TimePlugin,
62            ScheduleRunnerPlugin::default(),
63            LogPlugin {
64                level: self.log_level,
65                filter: self.log_filter.clone(),
66                custom_layer: crate::log::custom_layer,
67                ..Default::default()
68            },
69            AssetPlugin::default(),
70            ImagePlugin::default(),
71            StatesPlugin,
72        ));
73        app.init_resource::<LogTestSteps>();
74        app.init_resource::<TestRunnerTimeout>();
75
76        app.add_systems(
77            Update,
78            move |time: Res<Time<Real>>,
79                  timeout: Res<TestRunnerTimeout>,
80                  mut events: MessageWriter<AppExit>| {
81                let elapsed = time.elapsed_secs();
82                if elapsed > **timeout {
83                    error!("Timeout after {elapsed}s");
84                    events.write(AppExit::error());
85                }
86            },
87        );
88        app.add_systems(
89            PostUpdate,
90            (
91                log_step.run_if(resource_exists_and_equals(LogTestSteps(true))),
92                check_exit,
93            )
94                .chain(),
95        );
96
97        app.init_state::<Step>();
98    }
99}
100
101fn log_step(step: Res<State<Step>>, mut local_step: Local<u32>) {
102    info_once!("Step = {}", ***step); // when step = 0
103    if ***step != *local_step {
104        *local_step = ***step;
105        info!("Step = {}", ***step);
106    }
107}
108
109fn check_exit(mut reader: MessageReader<AppExit>) {
110    for msg in reader.read() {
111        error!("Obtained exit message {msg:?}");
112    }
113}
114
115#[test]
116fn timeout() {
117    let mut app = App::new();
118    app.add_plugins(TestRunnerPlugin::default());
119    app.insert_resource(TestRunnerTimeout(0.5));
120    assert!(app.run().is_error());
121}
122
123#[test]
124fn explicit_failure() {
125    let mut app = App::new();
126    app.add_plugins(TestRunnerPlugin::default());
127    app.add_systems(First, |mut commands: Commands| {
128        commands.write_message(AppExit::error());
129    });
130    assert!(app.run().is_error());
131}
132
133#[test]
134fn explicit_success() {
135    let mut app = App::new();
136    app.add_plugins(TestRunnerPlugin::default());
137    app.add_systems(First, |mut commands: Commands| {
138        commands.write_message(AppExit::Success);
139    });
140    assert!(app.run().is_success());
141}