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.
This commit is contained in:
François 2023-05-01 20:00:01 +02:00 committed by GitHub
parent 5288be7c6e
commit 8070c29c21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 71 additions and 6 deletions

View file

@ -1,3 +1,5 @@
( (
exit_after: Some(900) exit_after: Some(900),
frame_time: Some(0.03),
screenshot_frames: [200],
) )

View file

@ -1,3 +1,5 @@
( (
exit_after: Some(300) exit_after: Some(300),
frame_time: Some(0.03),
screenshot_frames: [100],
) )

View file

@ -199,13 +199,23 @@ jobs:
echo "running $example_name - "`date` 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" 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 sleep 10
if [ `find ./ -maxdepth 1 -name 'screenshot-*.png' -print -quit` ]; then
mkdir screenshots-$example_name
mv screenshot-*.png screenshots-$example_name/
fi
done done
zip traces.zip trace*.json zip traces.zip trace*.json
zip -r screenshots.zip screenshots-*
- name: save traces - name: save traces
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: example-traces.zip name: example-traces.zip
path: traces.zip path: traces.zip
- name: save screenshots
uses: actions/upload-artifact@v3
with:
name: screenshots.zip
path: screenshots.zip
- name: Save PR number - name: Save PR number
if: ${{ failure() && github.event_name == 'pull_request' }} if: ${{ failure() && github.event_name == 'pull_request' }}
run: | run: |

View file

@ -1,3 +1,5 @@
//! Utilities for testing in CI environments.
use crate::{app::AppExit, App, Update}; use crate::{app::AppExit, App, Update};
use serde::Deserialize; use serde::Deserialize;
@ -13,6 +15,11 @@ use bevy_utils::tracing::info;
pub struct CiTestingConfig { pub struct CiTestingConfig {
/// The number of frames after which Bevy should exit. /// The number of frames after which Bevy should exit.
pub exit_after: Option<u32>, pub exit_after: Option<u32>,
/// The time in seconds to update for each frame.
pub frame_time: Option<f32>,
/// Frames at which to capture a screenshot.
#[serde(default)]
pub screenshot_frames: Vec<u32>,
} }
fn ci_testing_exit_after( fn ci_testing_exit_after(

View file

@ -10,7 +10,7 @@ mod plugin_group;
mod schedule_runner; mod schedule_runner;
#[cfg(feature = "bevy_ci_testing")] #[cfg(feature = "bevy_ci_testing")]
mod ci_testing; pub mod ci_testing;
pub use app::*; pub use app::*;
pub use bevy_derive::DynamicPlugin; pub use bevy_derive::DynamicPlugin;

View file

@ -77,7 +77,7 @@ subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]
webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"] webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"]
# enable systems that allow for automated testing on CI # 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 # Enable animation support, and glTF animation loading
animation = ["bevy_animation", "bevy_gltf?/bevy_animation"] animation = ["bevy_animation", "bevy_gltf?/bevy_animation"]

View file

@ -17,6 +17,7 @@ jpeg = ["image/jpeg"]
bmp = ["image/bmp"] bmp = ["image/bmp"]
webp = ["image/webp"] webp = ["image/webp"]
dds = ["ddsfile"] dds = ["ddsfile"]
bevy_ci_testing = ["bevy_app/bevy_ci_testing"]
shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out"] shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out"]
shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"]

View file

@ -139,9 +139,37 @@ impl Plugin for ScreenshotPlugin {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>(); render_app.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>();
} }
#[cfg(feature = "bevy_ci_testing")]
if app
.world
.contains_resource::<bevy_app::ci_testing::CiTestingConfig>()
{
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<u32>,
ci_testing_config: bevy_ecs::prelude::Res<bevy_app::ci_testing::CiTestingConfig>,
mut screenshot_manager: ResMut<ScreenshotManager>,
main_window: Query<Entity, With<bevy_window::PrimaryWindow>>,
) {
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 { pub(crate) fn align_byte_size(value: u32) -> u32 {
value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT)) value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT))
} }

View file

@ -11,6 +11,7 @@ keywords = ["bevy"]
[features] [features]
default = [] default = []
serialize = ["serde"] serialize = ["serde"]
bevy_ci_testing = ["bevy_app/bevy_ci_testing"]
[dependencies] [dependencies]
# bevy # bevy

View file

@ -47,6 +47,19 @@ impl Plugin for TimePlugin {
.init_resource::<FixedTime>() .init_resource::<FixedTime>()
.add_systems(First, time_system.in_set(TimeSystem)) .add_systems(First, time_system.in_set(TimeSystem))
.add_systems(RunFixedUpdateLoop, run_fixed_update_schedule); .add_systems(RunFixedUpdateLoop, run_fixed_update_schedule);
#[cfg(feature = "bevy_ci_testing")]
if let Some(ci_testing_config) = app
.world
.get_resource::<bevy_app::ci_testing::CiTestingConfig>()
{
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, Automatic,
// Update [`Time`] with an exact `Instant` value // Update [`Time`] with an exact `Instant` value
ManualInstant(Instant), ManualInstant(Instant),
// Update [`Time`] with the current time + a specified `Duration` // Update [`Time`] with the last update time + a specified `Duration`
ManualDuration(Duration), ManualDuration(Duration),
} }
@ -107,7 +120,8 @@ fn time_system(
TimeUpdateStrategy::Automatic => time.update_with_instant(new_time), TimeUpdateStrategy::Automatic => time.update_with_instant(new_time),
TimeUpdateStrategy::ManualInstant(instant) => time.update_with_instant(*instant), TimeUpdateStrategy::ManualInstant(instant) => time.update_with_instant(*instant),
TimeUpdateStrategy::ManualDuration(duration) => { 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);
} }
} }
} }