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 1d78f6a73a..f2036f4a49 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 d170958d73..13f79f298c 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 f4b890ff8b..1db57174ae 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 f662a350a4..3ba0dde78e 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 ee409da3d1..f9ecd22569 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 4c828dd848..712ce1890a 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 7cebc4f021..aec631dde0 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 e201424686..b573c5269f 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -139,9 +139,37 @@ 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 { value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT)) } diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 3a7219ae04..7d4743b032 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 ee923f7b96..704d54d860 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); } } }