From 424e56318494ade4ed0f0e202e624515958a870f Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Tue, 15 Oct 2024 11:32:51 -0700 Subject: [PATCH] Make `contributors` example deterministic in CI (#15901) # Objective - Make the example deterministic when run with CI, so that the [screenshot comparison](https://thebevyflock.github.io/bevy-example-runner/) is stable - Preserve the "truly random on each run" behavior so that every page load in the example showcase shows a different contributor first ## Solution - Fall back to the static default contributor list in CI - Store contributors in a `Vec` so that we can show repeats of the fallback contributor list, giving the appearance of lots of overlapping contributors in CI - Use a shared seeded RNG throughout the app - Give contributor birds a `z` value so that their depth is stable - Remove the shuffle, which was redundant because contributors are first collected into a hashmap - `chain` the systems so that the physics is deterministic from run to run ## Testing ```bash echo '(setup: (fixed_frame_time: Some(0.05)), events: [(100, Screenshot), (500, AppExit)])' > config.ron CI_TESTING_CONFIG=config.ron cargo run --example contributors --features=bevy_ci_testing mv screenshot-100.png screenshot-100-a.png CI_TESTING_CONFIG=config.ron cargo run --example contributors --features=bevy_ci_testing diff screenshot-100.png screenshot-100-a.png ``` ## Alternatives I'd also be fine with removing this example from the list of examples that gets screenshot-tested in CI. Coverage from other 2d examples is probably adequate. --------- Co-authored-by: Alice Cecile --- examples/games/contributors.rs | 77 +++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/examples/games/contributors.rs b/examples/games/contributors.rs index 545f8babb7..2d653d496a 100644 --- a/examples/games/contributors.rs +++ b/examples/games/contributors.rs @@ -1,7 +1,8 @@ //! This example displays each contributor to the bevy source code as a bouncing bevy-ball. use bevy::{math::bounding::Aabb2d, prelude::*, utils::HashMap}; -use rand::{prelude::SliceRandom, Rng}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; use std::{ env::VarError, hash::{DefaultHasher, Hash, Hasher}, @@ -13,13 +14,14 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .init_resource::() + .init_resource::() .add_systems(Startup, (setup_contributor_selection, setup)) - .add_systems(Update, (gravity, movement, collisions, selection)) + // Systems are chained for determinism only + .add_systems(Update, (gravity, movement, collisions, selection).chain()) .run(); } -// Store contributors with their commit count in a collection that preserves the uniqueness -type Contributors = HashMap; +type Contributors = Vec<(String, usize)>; #[derive(Resource)] struct ContributorSelection { @@ -55,26 +57,34 @@ struct Velocity { rotation: f32, } +// We're using a shared seeded RNG here to make this example deterministic for testing purposes. +// This isn't strictly required in practical use unless you need your app to be deterministic. +#[derive(Resource, Deref, DerefMut)] +struct SharedRng(ChaCha8Rng); +impl Default for SharedRng { + fn default() -> Self { + Self(ChaCha8Rng::seed_from_u64(10223163112)) + } +} + const GRAVITY: f32 = 9.821 * 100.0; const SPRITE_SIZE: f32 = 75.0; const SELECTED: Hsla = Hsla::hsl(0.0, 0.9, 0.7); const DESELECTED: Hsla = Hsla::new(0.0, 0.3, 0.2, 0.92); +const SELECTED_Z_OFFSET: f32 = 100.0; + const SHOWCASE_TIMER_SECS: f32 = 3.0; const CONTRIBUTORS_LIST: &[&str] = &["Carter Anderson", "And Many More"]; -fn setup_contributor_selection(mut commands: Commands, asset_server: Res) { - // Load contributors from the git history log or use default values from - // the constant array. Contributors are stored in a HashMap with their - // commit count. - let contribs = contributors().unwrap_or_else(|_| { - CONTRIBUTORS_LIST - .iter() - .map(|name| (name.to_string(), 1)) - .collect() - }); +fn setup_contributor_selection( + mut commands: Commands, + asset_server: Res, + mut rng: ResMut, +) { + let contribs = contributors_or_fallback(); let texture_handle = asset_server.load("branding/icon.png"); @@ -83,11 +93,12 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res, mut velocity_query: Query<&mut Velocity>) { fn collisions( window: Single<&Window>, mut query: Query<(&mut Velocity, &mut Transform), With>, + mut rng: ResMut, ) { let window_size = window.size(); @@ -253,8 +263,6 @@ fn collisions( let max_bounce_height = (window_size.y - SPRITE_SIZE * 2.0).max(0.0); let min_bounce_height = max_bounce_height * 0.4; - let mut rng = rand::thread_rng(); - for (mut velocity, mut transform) in &mut query { // Clamp the translation to not go out of the bounds if transform.translation.y < collision_area.min.y { @@ -333,7 +341,26 @@ fn contributors() -> Result { }, ); - Ok(contributors) + Ok(contributors.into_iter().collect()) +} + +/// Get the contributors list, or fall back to a default value if +/// it's unavailable or we're in CI +fn contributors_or_fallback() -> Contributors { + let get_default = || { + CONTRIBUTORS_LIST + .iter() + .cycle() + .take(1000) + .map(|name| (name.to_string(), 1)) + .collect() + }; + + if cfg!(feature = "bevy_ci_testing") { + return get_default(); + } + + contributors().unwrap_or_else(|_| get_default()) } /// Give each unique contributor name a particular hue that is stable between runs.