Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utility functions for integration testing #3839

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
aeee113
Initial API outline
alice-i-cecile Feb 1, 2022
413b62a
Remove assert_function_on_* helpers
alice-i-cecile Feb 1, 2022
f08678d
More notes on lifetimes
alice-i-cecile Feb 1, 2022
4a03817
Removed cursed helper method
alice-i-cecile Feb 1, 2022
cd1e1e0
Add more docs
alice-i-cecile Feb 1, 2022
e63b4a7
Add wrapper methods for testing tools on `App`
alice-i-cecile Feb 1, 2022
95032ab
More doc examples
alice-i-cecile Feb 1, 2022
7d6552e
send_event convenience API
alice-i-cecile Feb 1, 2022
fd10738
Make sure doc tests compile
alice-i-cecile Feb 1, 2022
ded2267
Spaces > tabs
alice-i-cecile Feb 1, 2022
6f1c1f3
assert_system should use a Result
alice-i-cecile Feb 1, 2022
100784b
Integration testing stub
alice-i-cecile Feb 1, 2022
12f319c
Fleshed out integration test example
alice-i-cecile Feb 1, 2022
686526e
assert_component_eq helper method
alice-i-cecile Feb 1, 2022
693c1f1
More docs for assert_component_eq
alice-i-cecile Feb 1, 2022
8eb4246
Replace assert_n_in_query with query_len
alice-i-cecile Feb 1, 2022
5c62969
Warning about startup system footgun
alice-i-cecile Feb 1, 2022
42849fd
Thanks clippy
alice-i-cecile Feb 1, 2022
56fee27
assert_n_events -> events_len
alice-i-cecile Feb 1, 2022
341ca09
Split methods, and hide assertions behind test feature flag
alice-i-cecile Feb 1, 2022
34da21e
Improve error handling system name
alice-i-cecile Feb 1, 2022
1335372
Unhide assertion methods
alice-i-cecile Feb 1, 2022
038ccb4
Spaces >> tabs
alice-i-cecile Feb 1, 2022
751d42b
Fix missing imports
alice-i-cecile Feb 1, 2022
d90175d
, not ;
alice-i-cecile Feb 1, 2022
be7e0f4
Typo fix
alice-i-cecile Feb 1, 2022
2de9ca7
Typo fix
alice-i-cecile Feb 1, 2022
2ade9ae
Typo fix
alice-i-cecile Feb 1, 2022
d1d740c
Unwrap -> expect
alice-i-cecile Feb 1, 2022
00d3aee
Fix typo
alice-i-cecile Feb 1, 2022
9eb0418
Compare by reference
alice-i-cecile Feb 1, 2022
4abbb00
Fix generics
alice-i-cecile Feb 1, 2022
7d15c01
Thank you so much clippy
alice-i-cecile Feb 1, 2022
66a1613
QueryState::single doesn't exist yet
alice-i-cecile Feb 2, 2022
0976e67
Fix mistake in query_len method
alice-i-cecile Feb 2, 2022
20a9e4c
Integration test fixes
alice-i-cecile Feb 2, 2022
6b6a478
Demonstrate assertion on a resource
alice-i-cecile Feb 2, 2022
59ea602
Remove unnecessary type annotation
alice-i-cecile Feb 2, 2022
1f55d66
Remove trivial resource assertion helpers
alice-i-cecile Feb 2, 2022
809c2d9
Fix doc links
alice-i-cecile Feb 2, 2022
bcb9d1f
Resolve ambiguities
alice-i-cecile Feb 2, 2022
69a4e7a
Simpler input mocking
alice-i-cecile Feb 2, 2022
ba2b697
Also assert velocity is positive
alice-i-cecile Feb 2, 2022
2521be2
Identified why the integration test was failing
alice-i-cecile Feb 2, 2022
8a5d9e0
Send raw keyboard event
alice-i-cecile Feb 2, 2022
254ae06
Clippy fix
alice-i-cecile Feb 2, 2022
8422cdd
Fix outdated doc test
alice-i-cecile Feb 2, 2022
612d1f8
Fix doc tests
alice-i-cecile Feb 2, 2022
8bea4d2
Use new events.len method
alice-i-cecile Feb 4, 2022
65315db
More thoughtful events API
alice-i-cecile Feb 7, 2022
dd37ec1
Add invariant-checking plugin to example
alice-i-cecile Feb 7, 2022
3718e00
Remove assert_system
alice-i-cecile Feb 7, 2022
f241492
Remove assert_compont_eq
alice-i-cecile Feb 7, 2022
71b5fd4
Failed attempt at a stateless query helper
alice-i-cecile Feb 7, 2022
b19adf4
Revert "Failed attempt at a stateless query helper"
alice-i-cecile Feb 7, 2022
c3f0e5a
Revert "Remove assert_compont_eq"
alice-i-cecile Feb 7, 2022
bd2707c
Use delta-time physics
alice-i-cecile Feb 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{CoreStage, Events, Plugin, PluginGroup, PluginGroupBuilder, StartupS
pub use bevy_derive::AppLabel;
use bevy_ecs::{
prelude::{FromWorld, IntoExclusiveSystem},
query::{FilterFetch, WorldQuery},
schedule::{
IntoSystemDescriptor, RunOnce, Schedule, Stage, StageLabel, State, StateData, SystemSet,
SystemStage,
Expand Down Expand Up @@ -910,6 +911,48 @@ impl App {
}
}

// Testing adjacents tools
impl App {
/// Returns the number of entities found by the [`Query`](bevy_ecs::system::Query) with the type parameters `Q` and `F`
///
/// # Example
/// ```rust
/// # use bevy_app::App;
/// # use bevy_ecs::prelude::*;
///
/// #[derive(Component)]
/// struct Player;
///
/// #[derive(Component)]
/// struct Life(usize);
///
/// let mut app = App::new();
///
/// fn spawn_player(mut commands: Commands){
/// commands.spawn().insert(Life(10)).insert(Player);
/// }
///
/// app.add_startup_system(spawn_player);
/// assert_eq!(app.query_len::<&Life, With<Player>>(), 0);
///
/// // Run the `Schedule` once, causing our startup system to run
/// app.update();
/// assert_eq!(app.query_len::<&Life, With<Player>>(), 1);
///
/// // Running the schedule again won't cause startup systems to rerun
/// app.update();
/// assert_eq!(app.query_len::<&Life, With<Player>>(), 1);
/// ```
pub fn query_len<Q, F>(&mut self) -> usize
where
Q: WorldQuery,
F: WorldQuery,
<F as WorldQuery>::Fetch: FilterFetch,
{
self.world.query_len::<Q, F>()
}
}

fn run_once(mut app: App) {
app.update();
}
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod app;
mod plugin;
mod plugin_group;
mod schedule_runner;
mod testing_tools;

#[cfg(feature = "bevy_ci_testing")]
mod ci_testing;
Expand Down
71 changes: 71 additions & 0 deletions crates/bevy_app/src/testing_tools.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Tools for convenient integration testing of the ECS.
//!
//! Each of these methods has a corresponding method on `World`.

use crate::App;
use bevy_ecs::component::Component;
use bevy_ecs::query::{FilterFetch, WorldQuery};
use std::fmt::Debug;

impl App {
/// Asserts that all components of type `C` returned by a query with the filter `F` will equal `value`
///
/// This is commonly used with the corresponding `query_len` method to ensure that the returned query is not empty.
///
/// WARNING: because we are constructing the query from scratch,
/// [`Changed`](bevy_ecs::query::Changed) and [`Added`](bevy_ecs::query::Added) filters
/// will always return true.
///
/// # Example
/// ```rust
/// # use bevy_app::App;
/// # use bevy_ecs::prelude::*;
///
/// #[derive(Component)]
/// struct Player;
///
/// #[derive(Component, Debug, PartialEq)]
/// struct Life(usize);
///
/// let mut app = App::new();
///
/// fn spawn_player(mut commands: Commands){
/// commands.spawn().insert(Life(8)).insert(Player);
/// }
///
/// fn regenerate_life(mut query: Query<&mut Life>){
/// for mut life in query.iter_mut(){
/// if life.0 < 10 {
/// life.0 += 1;
/// }
/// }
/// }
///
/// app.add_startup_system(spawn_player).add_system(regenerate_life);
///
/// // Run the `Schedule` once, causing our startup system to run
/// // and life to regenerate once
/// app.update();
/// // The `()` value for `F` will result in an unfiltered query
/// app.assert_component_eq::<Life, ()>(&Life(9));
///
/// app.update();
/// // Because all of our entities with the `Life` component also
/// // have the `Player` component, these will be equivalent.
/// app.assert_component_eq::<Life, With<Player>>(&Life(10));
///
/// app.update();
/// // Check that life regeneration caps at 10, as intended
/// // Filtering by the component type you're looking for is useless,
/// // but it's helpful to demonstrate composing query filters here
/// app.assert_component_eq::<Life, (With<Player>, With<Life>)>(&Life(10));
/// ```
pub fn assert_component_eq<C, F>(&mut self, value: &C)
where
C: Component + PartialEq + Debug,
F: WorldQuery,
<F as WorldQuery>::Fetch: FilterFetch,
{
self.world.assert_component_eq::<C, F>(value);
}
}
20 changes: 20 additions & 0 deletions crates/bevy_ecs/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,26 @@ impl<T: Resource> Events<T> {
}
}

/// Iterate over all of the events in this collection
///
/// WARNING: This method is stateless, and, because events are double-buffered,
/// repeated calls (even in adjacent frames) will result in double-counting events.
///
/// In most cases, you want to create an `EventReader` to statefully track which events have been seen.
pub fn iter_stateless(&self) -> impl DoubleEndedIterator<Item = &T> {
let fresh_events = match self.state {
State::A => self.events_a.iter().map(map_instance_event),
State::B => self.events_b.iter().map(map_instance_event),
};

let old_events = match self.state {
State::B => self.events_a.iter().map(map_instance_event),
State::A => self.events_b.iter().map(map_instance_event),
};

old_events.chain(fresh_events)
}

/// Iterates over events that happened since the last "update" call.
/// WARNING: You probably don't want to use this call. In most cases you should use an
/// `EventReader`. You should only use this if you know you only need to consume events
Expand Down
45 changes: 45 additions & 0 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod entity_ref;
mod spawn_batch;
mod testing_tools;
mod world_cell;

pub use crate::change_detection::Mut;
Expand All @@ -13,6 +14,7 @@ use crate::{
change_detection::Ticks,
component::{Component, ComponentId, ComponentTicks, Components, StorageType},
entity::{AllocAtWithoutReplacement, Entities, Entity},
event::Events,
query::{FilterFetch, QueryState, WorldQuery},
storage::{Column, SparseSet, Storages},
system::Resource,
Expand Down Expand Up @@ -761,6 +763,35 @@ impl World {
self.get_non_send_unchecked_mut_with_id(component_id)
}

/// Get a mutable smart pointer to the [`Events`] [`Resource`] corresponding to type `E`
///
/// ```rust
/// use bevy_ecs::world::World;
/// use bevy_ecs::event::Events;
///
/// #[derive(Debug)]
/// struct Message(String);
///
/// let mut world = World::new();
/// world.insert_resource(Events::<Message>::default());
///
/// world.events::<Message>().send(Message("Hello World!".to_string()));
/// world.events::<Message>().send(Message("Welcome to Bevy!".to_string()));
///
/// // Cycles the event buffer; typically automatically done once each frame
/// // using `app.add_event::<E>()`
/// world.events::<Message>().update();
///
/// for event in world.events::<Message>().iter_stateless(){
/// dbg!(event);
/// }
/// ```
pub fn events<E: Resource>(&mut self) -> Mut<Events<E>> {
self.get_resource_mut::<Events<E>>().expect(
"No Events<E> resource found. Did you forget to call `.init_resource` or `.add_event`?",
)
}

/// For a given batch of ([Entity], [Bundle]) pairs, either spawns each [Entity] with the given
/// bundle (if the entity does not exist), or inserts the [Bundle] (if the entity already exists).
/// This is faster than doing equivalent operations one-by-one.
Expand Down Expand Up @@ -1144,6 +1175,20 @@ impl World {
}
}

// Testing-adjacent tools
impl World {
/// Returns the number of entities found by the [`Query`](crate::system::Query) with the type parameters `Q` and `F`
pub fn query_len<Q, F>(&mut self) -> usize
where
Q: WorldQuery,
F: WorldQuery,
<F as WorldQuery>::Fetch: FilterFetch,
{
let mut query_state = self.query_filtered::<Q, F>();
query_state.iter(self).count()
}
}

impl fmt::Debug for World {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("World")
Expand Down
34 changes: 34 additions & 0 deletions crates/bevy_ecs/src/world/testing_tools.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Tools for convenient integration testing of the ECS.
//!
//! Each of these methods has a corresponding method on `App`;
//! in many cases, these are more convenient to use.

use crate::component::Component;
use crate::entity::Entity;
use crate::world::{FilterFetch, World, WorldQuery};
use std::fmt::Debug;

impl World {
/// Asserts that all components of type `C` returned by a query with the filter `F` will equal `value`
///
/// This is commonly used with the corresponding `query_len` method to ensure that the returned query is not empty.
///
/// WARNING: because we are constructing the query from scratch,
/// [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters
/// will always return true.
pub fn assert_component_eq<C, F>(&mut self, value: &C)
where
C: Component + PartialEq + Debug,
F: WorldQuery,
<F as WorldQuery>::Fetch: FilterFetch,
{
let mut query_state = self.query_filtered::<(Entity, &C), F>();
for (entity, component) in query_state.iter(self) {
if component != value {
panic!(
"Found component {component:?} for {entity:?}, but was expecting {value:?}."
);
}
}
}
}
Loading