Skip to content

Commit b0409f6

Browse files
BD103alice-i-cecilemockersf
authored
Refactor ci_testing and separate it from DevToolsPlugin (#13513)
# Objective - We use [`ci_testing`](https://dev-docs.bevyengine.org/bevy/dev_tools/ci_testing/index.html) to specify per-example configuration on when to take a screenshot, when to exit, etc. - In the future more features may be added, such as #13512. To support this growth, `ci_testing` should be easier to read and maintain. ## Solution - Convert `ci_testing.rs` into the folder `ci_testing`, splitting the configuration and systems into `ci_testing/config.rs` and `ci_testing/systems.rs`. - Convert `setup_app` into the plugin `CiTestingPlugin`. This new plugin is added to both `DefaultPlugins` and `MinimalPlugins`. - Remove `DevToolsPlugin` from `MinimalPlugins`, since it was only used for CI testing. - Clean up some code, add many comments, and add a few unit tests. ## Testing The most important part is that this still passes all of the CI validation checks (merge queue), since that is when it will be used the most. I don't think I changed any behavior, so it should operate the same. You can also test it locally using: ```shell # Run the breakout example, enabling `bevy_ci_testing` and loading the configuration used in CI. CI_TESTING_CONFIG=".github/example-run/breakout.ron" cargo r --example breakout -F bevy_ci_testing ``` --- ## Changelog - Added `CiTestingPlugin`, which is split off from `DevToolsPlugin`. - Removed `DevToolsPlugin` from `MinimalPlugins`. ## Migration Guide Hi maintainers! I believe `DevToolsPlugin` was added within the same release as this PR, so I don't think a migration guide is needed. `DevToolsPlugin` is no longer included in `MinimalPlugins`, so you will need to remove it manually. ```rust // Before App::new() .add_plugins(MinimalPlugins) .run(); // After App::new() .add_plugins(MinimalPlugins) .add_plugins(DevToolsPlugin) .run(); ``` --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: François Mockers <[email protected]>
1 parent 7d843e0 commit b0409f6

File tree

6 files changed

+201
-135
lines changed

6 files changed

+201
-135
lines changed

crates/bevy_dev_tools/src/ci_testing.rs

Lines changed: 0 additions & 126 deletions
This file was deleted.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use bevy_ecs::prelude::*;
2+
use serde::Deserialize;
3+
4+
/// A configuration struct for automated CI testing.
5+
///
6+
/// It gets used when the `bevy_ci_testing` feature is enabled to automatically
7+
/// exit a Bevy app when run through the CI. This is needed because otherwise
8+
/// Bevy apps would be stuck in the game loop and wouldn't allow the CI to progress.
9+
#[derive(Deserialize, Resource, PartialEq, Debug)]
10+
pub struct CiTestingConfig {
11+
/// The setup for this test.
12+
#[serde(default)]
13+
pub setup: CiTestingSetup,
14+
/// Events to send, with their associated frame.
15+
#[serde(default)]
16+
pub events: Vec<CiTestingEventOnFrame>,
17+
}
18+
19+
/// Setup for a test.
20+
#[derive(Deserialize, Default, PartialEq, Debug)]
21+
pub struct CiTestingSetup {
22+
/// The amount of time in seconds between frame updates.
23+
///
24+
/// This is set through the [`TimeUpdateStrategy::ManualDuration`] resource.
25+
///
26+
/// [`TimeUpdateStrategy::ManualDuration`]: bevy_time::TimeUpdateStrategy::ManualDuration
27+
pub fixed_frame_time: Option<f32>,
28+
}
29+
30+
/// An event to send at a given frame, used for CI testing.
31+
#[derive(Deserialize, PartialEq, Debug)]
32+
pub struct CiTestingEventOnFrame(pub u32, pub CiTestingEvent);
33+
34+
/// An event to send, used for CI testing.
35+
#[derive(Deserialize, PartialEq, Debug)]
36+
pub enum CiTestingEvent {
37+
/// Takes a screenshot of the entire screen, and saves the results to
38+
/// `screenshot-{current_frame}.png`.
39+
Screenshot,
40+
/// Stops the program by sending [`AppExit::Success`].
41+
///
42+
/// [`AppExit::Success`]: bevy_app::AppExit::Success
43+
AppExit,
44+
/// Sends a [`CiTestingCustomEvent`] using the given [`String`].
45+
Custom(String),
46+
}
47+
48+
/// A custom event that can be configured from a configuration file for CI testing.
49+
#[derive(Event)]
50+
pub struct CiTestingCustomEvent(pub String);
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use super::*;
55+
56+
#[test]
57+
fn deserialize() {
58+
const INPUT: &str = r#"
59+
(
60+
setup: (
61+
fixed_frame_time: Some(0.03),
62+
),
63+
events: [
64+
(100, Custom("Hello, world!")),
65+
(200, Screenshot),
66+
(300, AppExit),
67+
],
68+
)"#;
69+
70+
let expected = CiTestingConfig {
71+
setup: CiTestingSetup {
72+
fixed_frame_time: Some(0.03),
73+
},
74+
events: vec![
75+
CiTestingEventOnFrame(100, CiTestingEvent::Custom("Hello, world!".into())),
76+
CiTestingEventOnFrame(200, CiTestingEvent::Screenshot),
77+
CiTestingEventOnFrame(300, CiTestingEvent::AppExit),
78+
],
79+
};
80+
81+
let config: CiTestingConfig = ron::from_str(INPUT).unwrap();
82+
83+
assert_eq!(config, expected);
84+
}
85+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! Utilities for testing in CI environments.
2+
3+
mod config;
4+
mod systems;
5+
6+
pub use self::config::*;
7+
8+
use bevy_app::prelude::*;
9+
use bevy_time::TimeUpdateStrategy;
10+
use std::time::Duration;
11+
12+
/// A plugin that instruments continuous integration testing by automatically executing user-defined actions.
13+
///
14+
/// This plugin reads a [`ron`] file specified with the `CI_TESTING_CONFIG` environmental variable
15+
/// (`ci_testing_config.ron` by default) and executes its specified actions. For a reference of the
16+
/// allowed configuration, see [`CiTestingConfig`].
17+
///
18+
/// This plugin is included within `DefaultPlugins` and `MinimalPlugins` when the `bevy_ci_testing`
19+
/// feature is enabled. It is recommended to only used this plugin during testing (manual or
20+
/// automatic), and disable it during regular development and for production builds.
21+
pub struct CiTestingPlugin;
22+
23+
impl Plugin for CiTestingPlugin {
24+
fn build(&self, app: &mut App) {
25+
#[cfg(not(target_arch = "wasm32"))]
26+
let config: CiTestingConfig = {
27+
let filename = std::env::var("CI_TESTING_CONFIG")
28+
.unwrap_or_else(|_| "ci_testing_config.ron".to_string());
29+
ron::from_str(
30+
&std::fs::read_to_string(filename)
31+
.expect("error reading CI testing configuration file"),
32+
)
33+
.expect("error deserializing CI testing configuration file")
34+
};
35+
36+
#[cfg(target_arch = "wasm32")]
37+
let config: CiTestingConfig = {
38+
let config = include_str!("../../../../ci_testing_config.ron");
39+
ron::from_str(config).expect("error deserializing CI testing configuration file")
40+
};
41+
42+
// Configure a fixed frame time if specified.
43+
if let Some(fixed_frame_time) = config.setup.fixed_frame_time {
44+
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32(
45+
fixed_frame_time,
46+
)));
47+
}
48+
49+
app.add_event::<CiTestingCustomEvent>()
50+
.insert_resource(config)
51+
.add_systems(Update, systems::send_events);
52+
}
53+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use super::config::*;
2+
use bevy_app::AppExit;
3+
use bevy_ecs::prelude::*;
4+
use bevy_render::view::screenshot::ScreenshotManager;
5+
use bevy_utils::tracing::{debug, info, warn};
6+
use bevy_window::PrimaryWindow;
7+
8+
pub(crate) fn send_events(world: &mut World, mut current_frame: Local<u32>) {
9+
let mut config = world.resource_mut::<CiTestingConfig>();
10+
11+
// Take all events for the current frame, leaving all the remaining alone.
12+
let events = std::mem::take(&mut config.events);
13+
let (to_run, remaining): (Vec<_>, _) = events
14+
.into_iter()
15+
.partition(|event| event.0 == *current_frame);
16+
config.events = remaining;
17+
18+
for CiTestingEventOnFrame(_, event) in to_run {
19+
debug!("Handling event: {:?}", event);
20+
match event {
21+
CiTestingEvent::AppExit => {
22+
world.send_event(AppExit::Success);
23+
info!("Exiting after {} frames. Test successful!", *current_frame);
24+
}
25+
CiTestingEvent::Screenshot => {
26+
let mut primary_window_query =
27+
world.query_filtered::<Entity, With<PrimaryWindow>>();
28+
let Ok(main_window) = primary_window_query.get_single(world) else {
29+
warn!("Requesting screenshot, but PrimaryWindow is not available");
30+
continue;
31+
};
32+
let Some(mut screenshot_manager) = world.get_resource_mut::<ScreenshotManager>()
33+
else {
34+
warn!("Requesting screenshot, but ScreenshotManager is not available");
35+
continue;
36+
};
37+
let path = format!("./screenshot-{}.png", *current_frame);
38+
screenshot_manager
39+
.save_screenshot_to_disk(main_window, path)
40+
.unwrap();
41+
info!("Took a screenshot at frame {}.", *current_frame);
42+
}
43+
// Custom events are forwarded to the world.
44+
CiTestingEvent::Custom(event_string) => {
45+
world.send_event(CiTestingCustomEvent(event_string));
46+
}
47+
}
48+
}
49+
50+
*current_frame += 1;
51+
}

crates/bevy_dev_tools/src/lib.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,5 @@ pub mod ui_debug_overlay;
4848
pub struct DevToolsPlugin;
4949

5050
impl Plugin for DevToolsPlugin {
51-
fn build(&self, _app: &mut App) {
52-
#[cfg(feature = "bevy_ci_testing")]
53-
{
54-
ci_testing::setup_app(_app);
55-
}
56-
}
51+
fn build(&self, _app: &mut App) {}
5752
}

crates/bevy_internal/src/default_plugins.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use bevy_app::{Plugin, PluginGroup, PluginGroupBuilder};
2929
/// * [`GilrsPlugin`](crate::gilrs::GilrsPlugin) - with feature `bevy_gilrs`
3030
/// * [`AnimationPlugin`](crate::animation::AnimationPlugin) - with feature `bevy_animation`
3131
/// * [`DevToolsPlugin`](crate::dev_tools::DevToolsPlugin) - with feature `bevy_dev_tools`
32+
/// * [`CiTestingPlugin`](crate::dev_tools::ci_testing::CiTestingPlugin) - with feature `bevy_ci_testing`
3233
///
3334
/// [`DefaultPlugins`] obeys *Cargo* *feature* flags. Users may exert control over this plugin group
3435
/// by disabling `default-features` in their `Cargo.toml` and enabling only those features
@@ -142,6 +143,11 @@ impl PluginGroup for DefaultPlugins {
142143
group = group.add(bevy_dev_tools::DevToolsPlugin);
143144
}
144145

146+
#[cfg(feature = "bevy_ci_testing")]
147+
{
148+
group = group.add(bevy_dev_tools::ci_testing::CiTestingPlugin);
149+
}
150+
145151
group = group.add(IgnoreAmbiguitiesPlugin);
146152

147153
group
@@ -173,7 +179,7 @@ impl Plugin for IgnoreAmbiguitiesPlugin {
173179
/// * [`FrameCountPlugin`](crate::core::FrameCountPlugin)
174180
/// * [`TimePlugin`](crate::time::TimePlugin)
175181
/// * [`ScheduleRunnerPlugin`](crate::app::ScheduleRunnerPlugin)
176-
/// * [`DevToolsPlugin`](crate::dev_tools::DevToolsPlugin) - with feature `bevy_dev_tools`
182+
/// * [`CiTestingPlugin`](crate::dev_tools::ci_testing::CiTestingPlugin) - with feature `bevy_ci_testing`
177183
///
178184
/// This group of plugins is intended for use for minimal, *headless* programs –
179185
/// see the [*Bevy* *headless* example](https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs)
@@ -194,10 +200,12 @@ impl PluginGroup for MinimalPlugins {
194200
.add(bevy_core::FrameCountPlugin)
195201
.add(bevy_time::TimePlugin)
196202
.add(bevy_app::ScheduleRunnerPlugin::default());
197-
#[cfg(feature = "bevy_dev_tools")]
203+
204+
#[cfg(feature = "bevy_ci_testing")]
198205
{
199-
group = group.add(bevy_dev_tools::DevToolsPlugin);
206+
group = group.add(bevy_dev_tools::ci_testing::CiTestingPlugin);
200207
}
208+
201209
group
202210
}
203211
}

0 commit comments

Comments
 (0)