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 33 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
99 changes: 99 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,104 @@ 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)
where
Q: WorldQuery,
F: WorldQuery,
<F as WorldQuery>::Fetch: FilterFetch,
{
self.world.query_len::<Q, F>();
}
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved

/// Sends an `event` of type `E`
///
/// # Example
/// ```rust
/// # use bevy_app::App;
/// # use bevy_ecs::prelude::*;
///
/// let mut app = App::new();
///
/// struct Message(String);
///
/// fn print_messages(mut messages: EventReader<Message>){
/// for message in messages.iter(){
/// println!("{}", message.0);
/// }
/// }
///
/// app.add_event::<Message>().add_system(print_messages);
/// app.send_event(Message("Hello!".to_string()));
///
/// // Says "Hello!"
/// app.update();
///
/// // All the events have been processed
/// app.update();
/// ```
pub fn send_event<E: Resource>(&mut self, event: E) {
self.world.send_event(event);
}

/// Returns the number of events of the type `E` that were sent this frame
///
/// # Example
/// ```rust
/// # use bevy_app::App;
/// # use bevy_ecs::prelude::*;
///
/// // An event type
/// #[derive(Debug)]
/// struct SelfDestruct;
///
/// let mut app = App::new();
/// app.add_event::<SelfDestruct>();
/// assert_eq!(app.n_events::<SelfDestruct>(), 0);
///
/// app.send_event(SelfDestruct);
/// assert_eq!(app.n_events::<SelfDestruct>(), 1);
///
/// // Time passes
/// app.update();
/// assert_eq!(app.n_events::<SelfDestruct>(), 0);
/// ```
pub fn events_len<E: Resource>(&self) -> usize {
self.world.events_len::<E>()
}
}

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
201 changes: 201 additions & 0 deletions crates/bevy_app/src/testing_tools.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
//! 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 bevy_ecs::system::IntoSystem;
use bevy_ecs::system::Resource;
use std::fmt::Debug;

impl App {
/// Asserts that the current value of the resource `R` is `value`
///
/// # Example
/// ```rust
/// # use bevy_app::App;
/// # use bevy_ecs::prelude::*;
///
/// // The resource we want to check the value of
/// #[derive(PartialEq, Debug)]
/// enum Toggle {
/// On,
/// Off,
/// }
///
/// let mut app = App::new();
///
/// // This system modifies our resource
/// fn toggle_off(mut toggle: ResMut<Toggle>) {
/// *toggle = Toggle::Off;
/// }
///
/// app.insert_resource(Toggle::On).add_system(toggle_off);
///
/// // Checking that the resource was initialized correctly
/// app.assert_resource_eq(Toggle::On);
///
/// // Run the `Schedule` once, causing our system to trigger
/// app.update();
///
/// // Checking that our resource was modified correctly
/// app.assert_resource_eq(Toggle::Off);
/// ```
pub fn assert_resource_eq<R: Resource + PartialEq + Debug>(&self, value: &R) {
self.world.assert_resource_eq(value);
}

/// Asserts that the current value of the non-send resource `NS` is `value`
pub fn assert_nonsend_resource_eq<NS: 'static + PartialEq + Debug>(&self, value: &NS) {
self.world.assert_nonsend_resource_eq(value);
}

/// 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.
///
/// # 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(8)).insert(Player);
/// }
///
/// fn regenerate_life(query: Query<&Life>){
/// for life in query.iter(){
/// 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);
}

/// Asserts that when the supplied `system` is run on the world, its output will be `Ok`
///
/// The `system` must return a `Result`: if the return value is an error the app will panic.
///
/// For more sophisticated error-handling, consider adding the system directly to the schedule
/// and using [system chaining](bevy_ecs::prelude::IntoChainSystem) to handle the result yourself.
///
/// WARNING: [`Changed`](bevy_ecs::query::Changed) and [`Added`](bevy_ecs::query::Added) filters
/// are computed relative to "the last time this system ran".
/// Because we are running a new system, these filters will always be true.
///
/// # Example
/// ```rust
/// # use bevy_app::App;
/// # use bevy_ecs::prelude::*;
///
/// #[derive(Component)]
/// struct Player;
///
/// #[derive(Component)]
/// struct Life(usize);
///
/// #[derive(Component)]
/// struct Dead;
///
/// let mut app = App::new();
///
/// fn spawn_player(mut commands: Commands){
/// commands.spawn().insert(Life(10)).insert(Player);
/// }
///
/// fn massive_damage(mut query: Query<&mut Life>){
/// for mut life in query.iter_mut(){
/// life.0 -= 9001;
/// }
/// }
///
/// fn kill_units(query: Query<(Entity, &Life)>, mut commands: Commands){
/// for (entity, life) in query.iter(){
/// if life.0 == 0 {
/// commands.entity(entity).insert(Dead);
/// }
/// }
/// }
///
/// app.add_startup_system(spawn_player)
/// .add_system(massive_damage)
/// .add_system(kill_units);
///
/// // Run the `Schedule` once, causing both our startup systems
/// // and ordinary systems to run once
/// app.update();
///
/// enum DeathError {
/// ZeroLifeIsNotDead,
/// DeadWithNonZeroLife,
/// }
///
/// // Run a complex assertion on the world using a system
/// fn zero_life_is_dead(query: Query<(&Life, Option<&Dead>)>) -> Result<(), DeathError> {
/// for (life, maybe_dead) in query.iter(){
/// if life.0 == 0 {
/// if maybe_dead.is_none(){
/// return Err(DeathError::ZeroLifeIsNotDead);
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
/// }
/// }
///
/// if maybe_dead.is_some(){
/// if life.0 != 0 {
/// return Err(DeathError::DeadWithNonZeroLife);
/// }
/// }
/// }
/// // None of our checks failed, so our world state is clean
/// true
/// }
///
/// app.update();
/// app.assert_system(zero_life_is_dead, None);
/// ```
pub fn assert_system<T: 'static, E: 'static, SystemParams>(
&mut self,
system: impl IntoSystem<(), Result<T, E>, SystemParams>,
) {
self.world.assert_system(system);
}
}
32 changes: 32 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 @@ -1144,6 +1146,36 @@ 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()
}

/// Sends an `event` of type `E`
pub fn send_event<E: Resource>(&mut self, event: E) {
let mut events: Mut<Events<E>> = self.get_resource_mut()
.expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::<E>()`?");

events.send(event);
}

/// Returns the number of events of the type `E` that were sent this frame
pub fn events_len<E: Resource>(&self) -> usize {
let events = self.get_resource::<Events<E>>()
.expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::<E>()`?");

events.iter_current_update_events().count()
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl fmt::Debug for World {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("World")
Expand Down
Loading