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#[derive(Resource, Deref, PartialEq)]
27pub struct TestRunnerTimeout(pub f32);
28impl Default for TestRunnerTimeout {
29 fn default() -> Self {
30 Self(5.)
31 }
32}
33
34#[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); 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}