diff --git a/.github/example-run/alien_cake_addict.ron b/.github/example-run/alien_cake_addict.ron index d170958d73..a8113dad04 100644 --- a/.github/example-run/alien_cake_addict.ron +++ b/.github/example-run/alien_cake_addict.ron @@ -1,3 +1,5 @@ ( - exit_after: Some(300) + events: [ + (300, AppExit), + ] ) diff --git a/.github/example-run/breakout.ron b/.github/example-run/breakout.ron index f2036f4a49..c12f84f0a0 100644 --- a/.github/example-run/breakout.ron +++ b/.github/example-run/breakout.ron @@ -1,5 +1,9 @@ ( - exit_after: Some(900), - frame_time: Some(0.03), - screenshot_frames: [200], + setup: ( + fixed_frame_time: Some(0.03), + ), + events: [ + (200, Screenshot), + (900, AppExit), + ] ) diff --git a/.github/example-run/contributors.ron b/.github/example-run/contributors.ron index 1d78f6a73a..2d50dc7fd0 100644 --- a/.github/example-run/contributors.ron +++ b/.github/example-run/contributors.ron @@ -1,3 +1,5 @@ ( - exit_after: Some(900) + events: [ + (900, AppExit), + ] ) diff --git a/.github/example-run/load_gltf.ron b/.github/example-run/load_gltf.ron index 13f79f298c..1ab6c9705c 100644 --- a/.github/example-run/load_gltf.ron +++ b/.github/example-run/load_gltf.ron @@ -1,5 +1,9 @@ ( - exit_after: Some(300), - frame_time: Some(0.03), - screenshot_frames: [100], + setup: ( + frame_time: Some(0.03), + ), + events: [ + (100, Screenshot), + (300, AppExit), + ] ) diff --git a/.github/example-run/no_renderer.ron b/.github/example-run/no_renderer.ron index 22e43495b5..b177e96633 100644 --- a/.github/example-run/no_renderer.ron +++ b/.github/example-run/no_renderer.ron @@ -1,3 +1,5 @@ ( - exit_after: Some(100) + events: [ + (100, AppExit), + ] ) diff --git a/.github/example-run/scene.ron b/.github/example-run/scene.ron index 22e43495b5..b177e96633 100644 --- a/.github/example-run/scene.ron +++ b/.github/example-run/scene.ron @@ -1,3 +1,5 @@ ( - exit_after: Some(100) + events: [ + (100, AppExit), + ] ) diff --git a/crates/bevy_dev_tools/src/ci_testing.rs b/crates/bevy_dev_tools/src/ci_testing.rs index 7a8bd8ecb8..e11a616032 100644 --- a/crates/bevy_dev_tools/src/ci_testing.rs +++ b/crates/bevy_dev_tools/src/ci_testing.rs @@ -1,16 +1,13 @@ //! Utilities for testing in CI environments. use bevy_app::{App, AppExit, Update}; -use bevy_ecs::{ - entity::Entity, - prelude::{resource_exists, Resource}, - query::With, - schedule::IntoSystemConfigs, - system::{Local, Query, Res, ResMut}, -}; +use bevy_ecs::prelude::*; use bevy_render::view::screenshot::ScreenshotManager; use bevy_time::TimeUpdateStrategy; -use bevy_utils::{tracing::info, Duration}; +use bevy_utils::{ + tracing::{debug, info, warn}, + Duration, +}; use bevy_window::PrimaryWindow; use serde::Deserialize; @@ -20,30 +17,39 @@ use serde::Deserialize; /// exit a Bevy app when run through the CI. This is needed because otherwise /// Bevy apps would be stuck in the game loop and wouldn't allow the CI to progress. #[derive(Deserialize, Resource)] -pub struct CiTestingConfig { - /// The number of frames after which Bevy should exit. - pub exit_after: Option, - /// The time in seconds to update for each frame. - pub frame_time: Option, - /// Frames at which to capture a screenshot. +struct CiTestingConfig { + /// The setup for this test. #[serde(default)] - pub screenshot_frames: Vec, + setup: CiTestingSetup, + /// Events to send, with their associated frame. + #[serde(default)] + events: Vec, } -fn ci_testing_exit_after( - mut current_frame: bevy_ecs::prelude::Local, - ci_testing_config: bevy_ecs::prelude::Res, - mut app_exit_events: bevy_ecs::event::EventWriter, -) { - if let Some(exit_after) = ci_testing_config.exit_after { - if *current_frame > exit_after { - app_exit_events.send(AppExit::Success); - info!("Exiting after {} frames. Test successful!", exit_after); - } - } - *current_frame += 1; +/// Setup for a test. +#[derive(Deserialize, Default)] +struct CiTestingSetup { + /// The time in seconds to update for each frame. + /// Set with the `TimeUpdateStrategy::ManualDuration(f32)` resource. + pub fixed_frame_time: Option, } +/// An event to send at a given frame, used for CI testing. +#[derive(Deserialize)] +pub struct CiTestingEventOnFrame(u32, CiTestingEvent); + +/// An event to send, used for CI testing. +#[derive(Deserialize, Debug)] +enum CiTestingEvent { + Screenshot, + AppExit, + Custom(String), +} + +/// A custom event that can be configured from a configuration file for CI testing. +#[derive(Event)] +pub struct CiTestingCustomEvent(pub String); + pub(crate) fn setup_app(app: &mut App) -> &mut App { #[cfg(not(target_arch = "wasm32"))] let config: CiTestingConfig = { @@ -61,39 +67,60 @@ pub(crate) fn setup_app(app: &mut App) -> &mut App { ron::from_str(config).expect("error deserializing CI testing configuration file") }; - if let Some(frame_time) = config.frame_time { + if let Some(fixed_frame_time) = config.setup.fixed_frame_time { app.world_mut() .insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32( - frame_time, + fixed_frame_time, ))); } - app.insert_resource(config).add_systems( - Update, - ( - ci_testing_exit_after, - ci_testing_screenshot_at.run_if(resource_exists::), - ), - ); + app.add_event::() + .insert_resource(config) + .add_systems(Update, send_events); app } -fn ci_testing_screenshot_at( - mut current_frame: Local, - ci_testing_config: Res, - mut screenshot_manager: ResMut, - main_window: Query>, -) { - if ci_testing_config - .screenshot_frames - .contains(&*current_frame) - { - info!("Taking a screenshot at frame {}.", *current_frame); - let path = format!("./screenshot-{}.png", *current_frame); - screenshot_manager - .save_screenshot_to_disk(main_window.single(), path) - .unwrap(); +fn send_events(world: &mut World, mut current_frame: Local) { + let mut config = world.resource_mut::(); + + let events = std::mem::take(&mut config.events); + let (to_run, remaining): (Vec<_>, _) = events + .into_iter() + .partition(|event| event.0 == *current_frame); + config.events = remaining; + + for CiTestingEventOnFrame(_, event) in to_run { + debug!("Handling event: {:?}", event); + match event { + CiTestingEvent::AppExit => { + world.send_event(AppExit::Success); + info!("Exiting after {} frames. Test successful!", *current_frame); + } + CiTestingEvent::Screenshot => { + let mut primary_window_query = + world.query_filtered::>(); + let Ok(main_window) = primary_window_query.get_single(world) else { + warn!("Requesting screenshot, but PrimaryWindow is not available"); + continue; + }; + let Some(mut screenshot_manager) = world.get_resource_mut::() + else { + warn!("Requesting screenshot, but ScreenshotManager is not available"); + continue; + }; + let path = format!("./screenshot-{}.png", *current_frame); + screenshot_manager + .save_screenshot_to_disk(main_window, path) + .unwrap(); + info!("Took a screenshot at frame {}.", *current_frame); + } + // Custom events are forwarded to the world. + CiTestingEvent::Custom(event_string) => { + world.send_event(CiTestingCustomEvent(event_string)); + } + } } + *current_frame += 1; } diff --git a/tools/build-wasm-example/src/main.rs b/tools/build-wasm-example/src/main.rs index 5ae1827db0..840ac8c011 100644 --- a/tools/build-wasm-example/src/main.rs +++ b/tools/build-wasm-example/src/main.rs @@ -50,7 +50,7 @@ fn main() { let mut features: Vec<&str> = cli.features.iter().map(|f| f.as_str()).collect(); if let Some(frames) = cli.frames { let mut file = File::create("ci_testing_config.ron").unwrap(); - file.write_fmt(format_args!("(exit_after: Some({frames}))")) + file.write_fmt(format_args!("(events: [({frames}, AppExit)])")) .unwrap(); features.push("bevy_ci_testing"); } diff --git a/tools/example-showcase/src/main.rs b/tools/example-showcase/src/main.rs index 9bf9d72d74..ad3b8ae4de 100644 --- a/tools/example-showcase/src/main.rs +++ b/tools/example-showcase/src/main.rs @@ -168,7 +168,7 @@ fn main() { (true, true) => { let mut file = File::create("example_showcase_config.ron").unwrap(); file.write_all( - b"(exit_after: None, frame_time: Some(0.05), screenshot_frames: [100])", + b"(setup: (fixed_frame_time: Some(0.05)), events: [(100, Screenshot)])", ) .unwrap(); extra_parameters.push("--features"); @@ -178,7 +178,7 @@ fn main() { (false, true) => { let mut file = File::create("example_showcase_config.ron").unwrap(); file.write_all( - b"(exit_after: Some(250), frame_time: Some(0.05), screenshot_frames: [100])", + b"(setup: (fixed_frame_time: Some(0.05)), events: [(100, Screenshot), (250, AppExit)])", ) .unwrap(); extra_parameters.push("--features"); @@ -186,7 +186,7 @@ fn main() { } (false, false) => { let mut file = File::create("example_showcase_config.ron").unwrap(); - file.write_all(b"(exit_after: Some(250))").unwrap(); + file.write_all(b"(events: [(250, AppExit)])").unwrap(); extra_parameters.push("--features"); extra_parameters.push("bevy_ci_testing"); }