mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 14:10:19 +00:00
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 <alice.i.cecile@gmail.com>
This commit is contained in:
parent
15440c189b
commit
424e563184
1 changed files with 52 additions and 25 deletions
|
@ -1,7 +1,8 @@
|
||||||
//! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
|
//! This example displays each contributor to the bevy source code as a bouncing bevy-ball.
|
||||||
|
|
||||||
use bevy::{math::bounding::Aabb2d, prelude::*, utils::HashMap};
|
use bevy::{math::bounding::Aabb2d, prelude::*, utils::HashMap};
|
||||||
use rand::{prelude::SliceRandom, Rng};
|
use rand::{Rng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha8Rng;
|
||||||
use std::{
|
use std::{
|
||||||
env::VarError,
|
env::VarError,
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
@ -13,13 +14,14 @@ fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins)
|
.add_plugins(DefaultPlugins)
|
||||||
.init_resource::<SelectionTimer>()
|
.init_resource::<SelectionTimer>()
|
||||||
|
.init_resource::<SharedRng>()
|
||||||
.add_systems(Startup, (setup_contributor_selection, setup))
|
.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();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store contributors with their commit count in a collection that preserves the uniqueness
|
type Contributors = Vec<(String, usize)>;
|
||||||
type Contributors = HashMap<String, usize>;
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct ContributorSelection {
|
struct ContributorSelection {
|
||||||
|
@ -55,26 +57,34 @@ struct Velocity {
|
||||||
rotation: f32,
|
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 GRAVITY: f32 = 9.821 * 100.0;
|
||||||
const SPRITE_SIZE: f32 = 75.0;
|
const SPRITE_SIZE: f32 = 75.0;
|
||||||
|
|
||||||
const SELECTED: Hsla = Hsla::hsl(0.0, 0.9, 0.7);
|
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 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 SHOWCASE_TIMER_SECS: f32 = 3.0;
|
||||||
|
|
||||||
const CONTRIBUTORS_LIST: &[&str] = &["Carter Anderson", "And Many More"];
|
const CONTRIBUTORS_LIST: &[&str] = &["Carter Anderson", "And Many More"];
|
||||||
|
|
||||||
fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetServer>) {
|
fn setup_contributor_selection(
|
||||||
// Load contributors from the git history log or use default values from
|
mut commands: Commands,
|
||||||
// the constant array. Contributors are stored in a HashMap with their
|
asset_server: Res<AssetServer>,
|
||||||
// commit count.
|
mut rng: ResMut<SharedRng>,
|
||||||
let contribs = contributors().unwrap_or_else(|_| {
|
) {
|
||||||
CONTRIBUTORS_LIST
|
let contribs = contributors_or_fallback();
|
||||||
.iter()
|
|
||||||
.map(|name| (name.to_string(), 1))
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
|
|
||||||
let texture_handle = asset_server.load("branding/icon.png");
|
let texture_handle = asset_server.load("branding/icon.png");
|
||||||
|
|
||||||
|
@ -83,11 +93,12 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
|
||||||
idx: 0,
|
idx: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
|
|
||||||
for (name, num_commits) in contribs {
|
for (name, num_commits) in contribs {
|
||||||
let transform =
|
let transform = Transform::from_xyz(
|
||||||
Transform::from_xyz(rng.gen_range(-400.0..400.0), rng.gen_range(0.0..400.0), 0.0);
|
rng.gen_range(-400.0..400.0),
|
||||||
|
rng.gen_range(0.0..400.0),
|
||||||
|
rng.gen(),
|
||||||
|
);
|
||||||
let dir = rng.gen_range(-1.0..1.0);
|
let dir = rng.gen_range(-1.0..1.0);
|
||||||
let velocity = Vec3::new(dir * 500.0, 0.0, 0.0);
|
let velocity = Vec3::new(dir * 500.0, 0.0, 0.0);
|
||||||
let hue = name_to_hue(&name);
|
let hue = name_to_hue(&name);
|
||||||
|
@ -120,8 +131,6 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res<AssetSe
|
||||||
contributor_selection.order.push(entity);
|
contributor_selection.order.push(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
contributor_selection.order.shuffle(&mut rng);
|
|
||||||
|
|
||||||
commands.insert_resource(contributor_selection);
|
commands.insert_resource(contributor_selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +217,7 @@ fn select(
|
||||||
) {
|
) {
|
||||||
sprite.color = SELECTED.with_hue(contributor.hue).into();
|
sprite.color = SELECTED.with_hue(contributor.hue).into();
|
||||||
|
|
||||||
transform.translation.z = 100.0;
|
transform.translation.z += SELECTED_Z_OFFSET;
|
||||||
|
|
||||||
writer.text(entity, 0).clone_from(&contributor.name);
|
writer.text(entity, 0).clone_from(&contributor.name);
|
||||||
*writer.text(entity, 1) = format!(
|
*writer.text(entity, 1) = format!(
|
||||||
|
@ -224,7 +233,7 @@ fn select(
|
||||||
fn deselect(sprite: &mut Sprite, contributor: &Contributor, transform: &mut Transform) {
|
fn deselect(sprite: &mut Sprite, contributor: &Contributor, transform: &mut Transform) {
|
||||||
sprite.color = DESELECTED.with_hue(contributor.hue).into();
|
sprite.color = DESELECTED.with_hue(contributor.hue).into();
|
||||||
|
|
||||||
transform.translation.z = 0.0;
|
transform.translation.z -= SELECTED_Z_OFFSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies gravity to all entities with a velocity.
|
/// Applies gravity to all entities with a velocity.
|
||||||
|
@ -244,6 +253,7 @@ fn gravity(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
|
||||||
fn collisions(
|
fn collisions(
|
||||||
window: Single<&Window>,
|
window: Single<&Window>,
|
||||||
mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>,
|
mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>,
|
||||||
|
mut rng: ResMut<SharedRng>,
|
||||||
) {
|
) {
|
||||||
let window_size = window.size();
|
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 max_bounce_height = (window_size.y - SPRITE_SIZE * 2.0).max(0.0);
|
||||||
let min_bounce_height = max_bounce_height * 0.4;
|
let min_bounce_height = max_bounce_height * 0.4;
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
|
|
||||||
for (mut velocity, mut transform) in &mut query {
|
for (mut velocity, mut transform) in &mut query {
|
||||||
// Clamp the translation to not go out of the bounds
|
// Clamp the translation to not go out of the bounds
|
||||||
if transform.translation.y < collision_area.min.y {
|
if transform.translation.y < collision_area.min.y {
|
||||||
|
@ -333,7 +341,26 @@ fn contributors() -> Result<Contributors, LoadContributorsError> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
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.
|
/// Give each unique contributor name a particular hue that is stable between runs.
|
||||||
|
|
Loading…
Reference in a new issue