From 8070c29c212d9bd3f9ad3fc537614b5982c276ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Mon, 1 May 2023 20:00:01 +0200 Subject: [PATCH] Take example screenshots in CI (#8488) # Objective - I want to take screenshots of examples in CI to help with validation of changes ## Solution - Can override how much time is updated per frame - Can specify on which frame to take a screenshots - Save screenshots in CI I reused the `TimeUpdateStrategy::ManualDuration` to be able to set the time update strategy to a fixed duration every frame. Its previous meaning didn't make much sense to me. This change makes it possible to have screenshots that are exactly the same across runs. If this gets merged, I'll add visual comparison of screenshots between runs to ensure nothing gets broken ## Migration Guide * `TimeUpdateStrategy::ManualDuration` meaning has changed. Instead of setting time to `Instant::now()` plus the given duration, it sets time to last update plus the given duration. --- .github/example-run/breakout.ron | 4 ++- .github/example-run/load_gltf.ron | 4 ++- .github/workflows/ci.yml | 10 +++++++ crates/bevy_app/src/ci_testing.rs | 7 +++++ crates/bevy_app/src/lib.rs | 2 +- crates/bevy_internal/Cargo.toml | 2 +- crates/bevy_render/Cargo.toml | 1 + .../bevy_render/src/view/window/screenshot.rs | 28 +++++++++++++++++++ crates/bevy_time/Cargo.toml | 1 + crates/bevy_time/src/lib.rs | 18 ++++++++++-- 10 files changed, 71 insertions(+), 6 deletions(-) diff --git a/.github/example-run/breakout.ron b/.github/example-run/breakout.ron index 1d78f6a73ad80..f2036f4a4986c 100644 --- a/.github/example-run/breakout.ron +++ b/.github/example-run/breakout.ron @@ -1,3 +1,5 @@ ( - exit_after: Some(900) + exit_after: Some(900), + frame_time: Some(0.03), + screenshot_frames: [200], ) diff --git a/.github/example-run/load_gltf.ron b/.github/example-run/load_gltf.ron index d170958d73bad..13f79f298c316 100644 --- a/.github/example-run/load_gltf.ron +++ b/.github/example-run/load_gltf.ron @@ -1,3 +1,5 @@ ( - exit_after: Some(300) + exit_after: Some(300), + frame_time: Some(0.03), + screenshot_frames: [100], ) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b890ff8bf33..1db57174ae857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,13 +199,23 @@ jobs: echo "running $example_name - "`date` time TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example xvfb-run cargo run --example $example_name --features "bevy_ci_testing,trace,trace_chrome" sleep 10 + if [ `find ./ -maxdepth 1 -name 'screenshot-*.png' -print -quit` ]; then + mkdir screenshots-$example_name + mv screenshot-*.png screenshots-$example_name/ + fi done zip traces.zip trace*.json + zip -r screenshots.zip screenshots-* - name: save traces uses: actions/upload-artifact@v3 with: name: example-traces.zip path: traces.zip + - name: save screenshots + uses: actions/upload-artifact@v3 + with: + name: screenshots.zip + path: screenshots.zip - name: Save PR number if: ${{ failure() && github.event_name == 'pull_request' }} run: | diff --git a/crates/bevy_app/src/ci_testing.rs b/crates/bevy_app/src/ci_testing.rs index f662a350a4177..3ba0dde78e307 100644 --- a/crates/bevy_app/src/ci_testing.rs +++ b/crates/bevy_app/src/ci_testing.rs @@ -1,3 +1,5 @@ +//! Utilities for testing in CI environments. + use crate::{app::AppExit, App, Update}; use serde::Deserialize; @@ -13,6 +15,11 @@ use bevy_utils::tracing::info; 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. + #[serde(default)] + pub screenshot_frames: Vec, } fn ci_testing_exit_after( diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index ee409da3d1532..f9ecd22569655 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -10,7 +10,7 @@ mod plugin_group; mod schedule_runner; #[cfg(feature = "bevy_ci_testing")] -mod ci_testing; +pub mod ci_testing; pub use app::*; pub use bevy_derive::DynamicPlugin; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 4c828dd848b68..712ce1890aca3 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -77,7 +77,7 @@ subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"] # enable systems that allow for automated testing on CI -bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render?/ci_limits"] +bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_time/bevy_ci_testing", "bevy_render?/bevy_ci_testing", "bevy_render?/ci_limits"] # Enable animation support, and glTF animation loading animation = ["bevy_animation", "bevy_gltf?/bevy_animation"] diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 7cebc4f0218f6..aec631dde0f8c 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -17,6 +17,7 @@ jpeg = ["image/jpeg"] bmp = ["image/bmp"] webp = ["image/webp"] dds = ["ddsfile"] +bevy_ci_testing = ["bevy_app/bevy_ci_testing"] shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index e201424686330..b573c5269ff95 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -139,7 +139,35 @@ impl Plugin for ScreenshotPlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } + + #[cfg(feature = "bevy_ci_testing")] + if app + .world + .contains_resource::() + { + app.add_systems(bevy_app::Update, ci_testing_screenshot_at); + } + } +} + +#[cfg(feature = "bevy_ci_testing")] +fn ci_testing_screenshot_at( + mut current_frame: bevy_ecs::prelude::Local, + ci_testing_config: bevy_ecs::prelude::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(); } + *current_frame += 1; } pub(crate) fn align_byte_size(value: u32) -> u32 { diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 3a7219ae042ca..7d4743b032834 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["bevy"] [features] default = [] serialize = ["serde"] +bevy_ci_testing = ["bevy_app/bevy_ci_testing"] [dependencies] # bevy diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index ee923f7b964ce..704d54d860452 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -47,6 +47,19 @@ impl Plugin for TimePlugin { .init_resource::() .add_systems(First, time_system.in_set(TimeSystem)) .add_systems(RunFixedUpdateLoop, run_fixed_update_schedule); + + #[cfg(feature = "bevy_ci_testing")] + if let Some(ci_testing_config) = app + .world + .get_resource::() + { + if let Some(frame_time) = ci_testing_config.frame_time { + app.world + .insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32( + frame_time, + ))); + } + } } } @@ -60,7 +73,7 @@ pub enum TimeUpdateStrategy { Automatic, // Update [`Time`] with an exact `Instant` value ManualInstant(Instant), - // Update [`Time`] with the current time + a specified `Duration` + // Update [`Time`] with the last update time + a specified `Duration` ManualDuration(Duration), } @@ -107,7 +120,8 @@ fn time_system( TimeUpdateStrategy::Automatic => time.update_with_instant(new_time), TimeUpdateStrategy::ManualInstant(instant) => time.update_with_instant(*instant), TimeUpdateStrategy::ManualDuration(duration) => { - time.update_with_instant(Instant::now() + *duration); + let last_update = time.last_update().unwrap_or_else(|| time.startup()); + time.update_with_instant(last_update + *duration); } } }