From aeee11304edf2a1e12687862fbd81bbddf598540 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 01:39:28 -0500 Subject: [PATCH 01/57] Initial API outline --- crates/bevy_ecs/src/world/mod.rs | 1 + crates/bevy_ecs/src/world/testing_tools.rs | 102 +++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 crates/bevy_ecs/src/world/testing_tools.rs diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index c07892a35243a..a467e56b7a8c6 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,5 +1,6 @@ mod entity_ref; mod spawn_batch; +mod testing_tools; mod world_cell; pub use crate::change_detection::Mut; diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs new file mode 100644 index 0000000000000..f076652d102e0 --- /dev/null +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -0,0 +1,102 @@ +use crate::event::Events; +use crate::query::{Fetch, WorldQuery}; +use crate::schedule::{Stage, SystemStage}; +use crate::system::{In, IntoChainSystem, IntoSystem}; +use crate::world::{FilterFetch, Resource, World}; +use std::fmt::Debug; + +impl World { + /// Asserts that that the current value of the resource `R` is `value` + pub fn assert_resource_eq(&self, value: R) { + let resource = self + .get_resource::() + .expect("No resource matching the type of {value} was found in the world."); + assert_eq!(*resource, value); + } + + /// Asserts that that the current value of the non-send resource `NS` is `value` + pub fn assert_nonsend_resource_eq(&self, value: NS) { + let resource = self + .get_non_send_resource::() + .expect("No non-send resource matching the type of {value} was found in the world."); + assert_eq!(*resource, value); + } + + /// Asserts that each item returned by the provided query is equal to the provided `value` + pub fn assert_query_eq<'w, 's, Q, F>( + &mut self, + value: >::Item, + ) where + Q: WorldQuery, + F: WorldQuery, + >::Item: PartialEq + Debug, + ::Fetch: FilterFetch, + { + let query_state = self.query_filtered::(); + for item in query_state.iter(self) { + // FIXME: lifetime mismatch + // ...but data from `self` flows into `value` here + assert_eq!(item, value); + } + } + + /// Asserts that when the provided `func` is applied to each item in the query, the result is `true` + pub fn assert_function_on_query<'w, 's, Q, F, Func>(&mut self, func: Func) + where + Q: WorldQuery, + F: WorldQuery, + ::Fetch: FilterFetch, + Func: Fn(>::Item) -> bool, + { + let query_state = self.query_filtered::(); + // FIXME: cannot infer an appropriate lifetime for lifetime parameter 'w in function call due to conflicting requirements + // expected `<::ReadOnlyFetch as fetch::Fetch<'w, 's>>::Item` + // found `<::ReadOnlyFetch as fetch::Fetch<'_, '_>>::Item` + for item in query_state.iter(self) { + assert!(func(item)); + } + } + + /// Asserts that the number of entities returned by the query is exactly `n` + pub fn assert_n_in_query(&mut self, n: usize) + where + Q: WorldQuery, + F: WorldQuery, + ::Fetch: FilterFetch, + { + let query_state = self.query_filtered::(); + assert_eq!(query_state.iter(self).count(), n); + } + + /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` + pub fn assert_n_events(&self, n: usize) { + let events = self.get_resource::>().unwrap(); + + assert_eq!(events.iter_current_update_events().count(), n); + } + + /// Asserts that, for each event of type `E`, the result of `func(event)` is `true` + pub fn assert_function_on_events(&self, func: Func) + where + Func: Fn(&E) -> bool, + { + let events = self.get_resource::>().unwrap(); + let mut reader = events.get_reader(); + + for event in reader.iter(events) { + assert!(func(event)); + } + } + + /// Asserts that when the supplied `system` is run on the world, its output will be `true` + pub fn assert_system(&mut self, system: impl IntoSystem<(), bool, Params>) { + let mut stage = SystemStage::single_threaded(); + stage.add_system(system.chain(assert_system_input_true)); + stage.run(self); + } +} + +/// A chainable system that panics if its `input` is not `true` +fn assert_system_input_true(In(result): In) { + assert!(result); +} From 413b62aa858759c483a7ed0e8f51836c66412cb3 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 02:05:43 -0500 Subject: [PATCH 02/57] Remove assert_function_on_* helpers --- crates/bevy_ecs/src/world/testing_tools.rs | 32 +--------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index f076652d102e0..dbc22fa609291 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -24,7 +24,7 @@ impl World { /// Asserts that each item returned by the provided query is equal to the provided `value` pub fn assert_query_eq<'w, 's, Q, F>( - &mut self, + &'s mut self, value: >::Item, ) where Q: WorldQuery, @@ -40,23 +40,6 @@ impl World { } } - /// Asserts that when the provided `func` is applied to each item in the query, the result is `true` - pub fn assert_function_on_query<'w, 's, Q, F, Func>(&mut self, func: Func) - where - Q: WorldQuery, - F: WorldQuery, - ::Fetch: FilterFetch, - Func: Fn(>::Item) -> bool, - { - let query_state = self.query_filtered::(); - // FIXME: cannot infer an appropriate lifetime for lifetime parameter 'w in function call due to conflicting requirements - // expected `<::ReadOnlyFetch as fetch::Fetch<'w, 's>>::Item` - // found `<::ReadOnlyFetch as fetch::Fetch<'_, '_>>::Item` - for item in query_state.iter(self) { - assert!(func(item)); - } - } - /// Asserts that the number of entities returned by the query is exactly `n` pub fn assert_n_in_query(&mut self, n: usize) where @@ -75,19 +58,6 @@ impl World { assert_eq!(events.iter_current_update_events().count(), n); } - /// Asserts that, for each event of type `E`, the result of `func(event)` is `true` - pub fn assert_function_on_events(&self, func: Func) - where - Func: Fn(&E) -> bool, - { - let events = self.get_resource::>().unwrap(); - let mut reader = events.get_reader(); - - for event in reader.iter(events) { - assert!(func(event)); - } - } - /// Asserts that when the supplied `system` is run on the world, its output will be `true` pub fn assert_system(&mut self, system: impl IntoSystem<(), bool, Params>) { let mut stage = SystemStage::single_threaded(); From f08678df799a1e378c4f6b83b5593624f8dd2a5a Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 02:34:20 -0500 Subject: [PATCH 03/57] More notes on lifetimes --- crates/bevy_ecs/src/world/testing_tools.rs | 26 ++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index dbc22fa609291..3f8ece51e5f34 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -23,20 +23,28 @@ impl World { } /// Asserts that each item returned by the provided query is equal to the provided `value` - pub fn assert_query_eq<'w, 's, Q, F>( - &'s mut self, - value: >::Item, + pub fn assert_query_items_eq<'w, 's1, 's2, Q, F>( + // Reference to the world must live at least as long as the query state + // FIXME: first lifetime points to lifetime on &mut self + &'s2 mut self, + // Reference to the value must live at least as long as the query state + // FIXME: Second lifetime points to lifetime on Item + value: &'s2 >::Item, ) where Q: WorldQuery, F: WorldQuery, - >::Item: PartialEq + Debug, + >::Item: PartialEq + Debug, ::Fetch: FilterFetch, + // World must outlive query state + 'w: 's1, + 'w: 's2, { - let query_state = self.query_filtered::(); + let mut query_state = self.query_filtered::(); for item in query_state.iter(self) { - // FIXME: lifetime mismatch - // ...but data from `self` flows into `value` here - assert_eq!(item, value); + // item has lifetimes 'w, 's1 + // value has lifetimes 'w, 's2 + // FIXME: the lifetime `'s1` does not necessarily outlive the lifetime `'s2` + assert_eq!(item, *value); } } @@ -47,7 +55,7 @@ impl World { F: WorldQuery, ::Fetch: FilterFetch, { - let query_state = self.query_filtered::(); + let mut query_state = self.query_filtered::(); assert_eq!(query_state.iter(self).count(), n); } From 4a0381769a6a28b6a574d8f914c88a86eddfc057 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 13:17:04 -0500 Subject: [PATCH 04/57] Removed cursed helper method --- crates/bevy_ecs/src/world/testing_tools.rs | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 3f8ece51e5f34..3d65e047d5957 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -22,32 +22,6 @@ impl World { assert_eq!(*resource, value); } - /// Asserts that each item returned by the provided query is equal to the provided `value` - pub fn assert_query_items_eq<'w, 's1, 's2, Q, F>( - // Reference to the world must live at least as long as the query state - // FIXME: first lifetime points to lifetime on &mut self - &'s2 mut self, - // Reference to the value must live at least as long as the query state - // FIXME: Second lifetime points to lifetime on Item - value: &'s2 >::Item, - ) where - Q: WorldQuery, - F: WorldQuery, - >::Item: PartialEq + Debug, - ::Fetch: FilterFetch, - // World must outlive query state - 'w: 's1, - 'w: 's2, - { - let mut query_state = self.query_filtered::(); - for item in query_state.iter(self) { - // item has lifetimes 'w, 's1 - // value has lifetimes 'w, 's2 - // FIXME: the lifetime `'s1` does not necessarily outlive the lifetime `'s2` - assert_eq!(item, *value); - } - } - /// Asserts that the number of entities returned by the query is exactly `n` pub fn assert_n_in_query(&mut self, n: usize) where From cd1e1e0bfdaf62f8768641a44eb8a2386687218d Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 13:17:24 -0500 Subject: [PATCH 05/57] Add more docs --- crates/bevy_ecs/src/world/testing_tools.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 3d65e047d5957..d1bd7230c758d 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -1,8 +1,11 @@ +//! 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::event::Events; -use crate::query::{Fetch, WorldQuery}; use crate::schedule::{Stage, SystemStage}; use crate::system::{In, IntoChainSystem, IntoSystem}; -use crate::world::{FilterFetch, Resource, World}; +use crate::world::{FilterFetch, Resource, World, WorldQuery}; use std::fmt::Debug; impl World { @@ -41,6 +44,9 @@ impl World { } /// Asserts that when the supplied `system` is run on the world, its output will be `true` + /// + /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". + /// Because we are generating a new system; these filters will always be true. pub fn assert_system(&mut self, system: impl IntoSystem<(), bool, Params>) { let mut stage = SystemStage::single_threaded(); stage.add_system(system.chain(assert_system_input_true)); From e63b4a7e6ee7d6c952e516051ef77e61411a0271 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 13:24:24 -0500 Subject: [PATCH 06/57] Add wrapper methods for testing tools on `App` --- crates/bevy_app/src/lib.rs | 1 + crates/bevy_app/src/testing_tools.rs | 65 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 crates/bevy_app/src/testing_tools.rs diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index a891c5957b786..6dc9e850978c9 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -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; diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs new file mode 100644 index 0000000000000..3036e4b41223d --- /dev/null +++ b/crates/bevy_app/src/testing_tools.rs @@ -0,0 +1,65 @@ +use crate::App; +use bevy_ecs::query::{FilterFetch, WorldQuery}; +use bevy_ecs::system::IntoSystem; +use bevy_ecs::system::Resource; +use std::fmt::Debug; + +impl App { + /// Asserts that that the current value of the resource `R` is `value` + /// + /// # Example + /// ```rust + /// use bevy::prelude::*; + /// + /// // The resource we want to check the value of + /// enum Toggle{ + /// On, + /// Off, + /// } + /// + /// let mut app = App::new(); + /// + /// // This system modifies our resource + /// fn toggle_off() + /// + /// app.insert_resource(Toggle::On).add_system(toggle_off); + /// + /// app.assert_resource_eq(Toggle::On); + /// + /// // Run the `Schedule` once, causing our system to trigger + /// app.update(); + /// + /// app.assert_resource_eq(Toggle::Off); + /// ``` + pub fn assert_resource_eq(&self, value: R) { + self.world.assert_resource_eq(value); + } + + /// Asserts that that the current value of the non-send resource `NS` is `value` + pub fn assert_nonsend_resource_eq(&self, value: NS) { + self.world.assert_nonsend_resource_eq(value); + } + + /// Asserts that the number of entities returned by the query is exactly `n` + pub fn assert_n_in_query(&mut self, n: usize) + where + Q: WorldQuery, + F: WorldQuery, + ::Fetch: FilterFetch, + { + self.world.assert_n_in_query::(n); + } + + /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` + pub fn assert_n_events(&self, n: usize) { + self.world.assert_n_events::(n); + } + + /// Asserts that when the supplied `system` is run on the world, its output will be `true` + /// + /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". + /// Because we are generating a new system; these filters will always be true. + pub fn assert_system(&mut self, system: impl IntoSystem<(), bool, Params>) { + self.world.assert_system(system); + } +} From 95032ab42738e22df07c3586e765def6cd58fa31 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 13:51:20 -0500 Subject: [PATCH 07/57] More doc examples --- crates/bevy_app/src/testing_tools.rs | 98 ++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 3036e4b41223d..90a406f14bd6b 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -24,11 +24,13 @@ impl App { /// /// 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(&self, value: R) { @@ -41,6 +43,34 @@ impl App { } /// Asserts that the number of entities returned by the query is exactly `n` + /// + /// # Example + /// ```rust + /// use bevy::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); + /// app.assert_n_in_query::<&Life, With>(0); + /// + /// // Run the `Schedule` once, causing our startup system to run + /// app.update(); + /// app.assert_n_in_query::<&Life, With>(1); + /// + /// // Running the schedule again won't cause startup systems to rerun + /// app.update(); + /// app.assert_n_in_query::<&Life, With>(1); + /// ``` pub fn assert_n_in_query(&mut self, n: usize) where Q: WorldQuery, @@ -51,6 +81,10 @@ impl App { } /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` + /// + /// # Example + /// ```rust + /// ``` pub fn assert_n_events(&self, n: usize) { self.world.assert_n_events::(n); } @@ -59,6 +93,70 @@ impl App { /// /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". /// Because we are generating a new system; these filters will always be true. + /// + /// # Example + /// ```rust + /// use bevy::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, 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(); + /// + /// // Run a complex assertion on the world using a system + /// fn zero_life_is_dead(query: Query<&Life, Option<&Dead>>) -> bool { + /// for (life, maybe_dead) in query.iter(){ + /// if life.0 == 0 { + /// if maybe_dead.is_none(){ + /// return false; + /// } + /// } + /// + /// if maybe_dead.is_some(){ + /// if life.0 != 0 { + /// return false; + /// } + /// } + /// } + /// // None of our checks failed, so our world state is clean + /// true + /// } + /// + /// app.update(); + /// app.assert_system(zero_life_is_dead); + /// ``` pub fn assert_system(&mut self, system: impl IntoSystem<(), bool, Params>) { self.world.assert_system(system); } From 7d6552e89c6f661f6fe5d464654e5c8d1bae0342 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 14:08:15 -0500 Subject: [PATCH 08/57] send_event convenience API --- crates/bevy_app/src/testing_tools.rs | 43 ++++++++++++++++++++++ crates/bevy_ecs/src/world/testing_tools.rs | 10 ++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 90a406f14bd6b..b95f33b115243 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -80,10 +80,53 @@ impl App { self.world.assert_n_in_query::(n); } + /// Sends an `event` of type `E` + /// + /// # Example + /// ```rust + /// use bevy::prelude::*; + /// + /// let app = App::new(); + /// + /// struct Message(String); + /// + /// fn print_messages(messages: EventReader){ + /// for message in messages.iter(){ + /// println!(message); + /// } + /// } + /// + /// app.add_event::().add_system(print_messages); + /// app.send_event(Message("Hello!")); + /// + /// // Says "Hello!" + /// app.update(); + /// + /// // All the events have been processed + /// app.update(); + /// ``` + pub fn send_event(&mut self, event: E) { + self.world.send_event(event); + } + /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` /// /// # Example /// ```rust + /// use bevy_app::App; + /// + /// // An event type + /// struct SelfDestruct; + /// + /// let mut app = App::new().add_event::(); + /// app.assert_n_events::(0); + /// + /// app.send_event(SelfDestruct); + /// app.assert_n_events::(1); + /// + /// // Time passes + /// app.update(); + /// app.assert_n_events::(0); /// ``` pub fn assert_n_events(&self, n: usize) { self.world.assert_n_events::(n); diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index d1bd7230c758d..9c9b05fbac066 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -5,7 +5,7 @@ use crate::event::Events; use crate::schedule::{Stage, SystemStage}; use crate::system::{In, IntoChainSystem, IntoSystem}; -use crate::world::{FilterFetch, Resource, World, WorldQuery}; +use crate::world::{FilterFetch, Mut, Resource, World, WorldQuery}; use std::fmt::Debug; impl World { @@ -36,6 +36,14 @@ impl World { assert_eq!(query_state.iter(self).count(), n); } + /// Sends an `event` of type `E` + pub fn send_event(&mut self, event: E) { + let mut events: Mut> = self.get_resource_mut() + .expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::()`?"); + + events.send(event); + } + /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` pub fn assert_n_events(&self, n: usize) { let events = self.get_resource::>().unwrap(); From fd1073838b4d9d21e551ed32d349bf7016ca1ded Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 14:29:33 -0500 Subject: [PATCH 09/57] Make sure doc tests compile --- crates/bevy_app/src/testing_tools.rs | 44 +++++++++++++--------- crates/bevy_ecs/src/world/testing_tools.rs | 2 +- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index b95f33b115243..a6196b0910902 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -9,10 +9,12 @@ impl App { /// /// # Example /// ```rust - /// use bevy::prelude::*; + /// # use bevy_app::App; + /// # use bevy_ecs::prelude::*; /// /// // The resource we want to check the value of - /// enum Toggle{ + /// #[derive(PartialEq, Debug)] + /// enum Toggle { /// On, /// Off, /// } @@ -20,7 +22,9 @@ impl App { /// let mut app = App::new(); /// /// // This system modifies our resource - /// fn toggle_off() + /// fn toggle_off(mut toggle: ResMut) { + /// *toggle = Toggle::Off; + /// } /// /// app.insert_resource(Toggle::On).add_system(toggle_off); /// @@ -46,7 +50,8 @@ impl App { /// /// # Example /// ```rust - /// use bevy::prelude::*; + /// # use bevy_app::App; + /// # use bevy_ecs::prelude::*; /// /// #[derive(Component)] /// struct Player; @@ -57,7 +62,7 @@ impl App { /// let mut app = App::new(); /// /// fn spawn_player(mut commands: Commands){ - /// commands.spawn().insert(Life(10).insert(Player); + /// commands.spawn().insert(Life(10)).insert(Player); /// } /// /// app.add_startup_system(spawn_player); @@ -84,20 +89,21 @@ impl App { /// /// # Example /// ```rust - /// use bevy::prelude::*; + /// # use bevy_app::App; + /// # use bevy_ecs::prelude::*; /// - /// let app = App::new(); + /// let mut app = App::new(); /// /// struct Message(String); /// - /// fn print_messages(messages: EventReader){ + /// fn print_messages(mut messages: EventReader){ /// for message in messages.iter(){ - /// println!(message); + /// println!("{}", message.0); /// } /// } /// /// app.add_event::().add_system(print_messages); - /// app.send_event(Message("Hello!")); + /// app.send_event(Message("Hello!".to_string())); /// /// // Says "Hello!" /// app.update(); @@ -113,12 +119,15 @@ impl App { /// /// # Example /// ```rust - /// use bevy_app::App; + /// # use bevy_app::App; + /// # use bevy_ecs::prelude::*; /// /// // An event type + /// #[derive(Debug)] /// struct SelfDestruct; /// - /// let mut app = App::new().add_event::(); + /// let mut app = App::new(); + /// app.add_event::(); /// app.assert_n_events::(0); /// /// app.send_event(SelfDestruct); @@ -128,7 +137,7 @@ impl App { /// app.update(); /// app.assert_n_events::(0); /// ``` - pub fn assert_n_events(&self, n: usize) { + pub fn assert_n_events(&self, n: usize) { self.world.assert_n_events::(n); } @@ -139,7 +148,8 @@ impl App { /// /// # Example /// ```rust - /// use bevy::prelude::*; + /// # use bevy_app::App; + /// # use bevy_ecs::prelude::*; /// /// #[derive(Component)] /// struct Player; @@ -153,7 +163,7 @@ impl App { /// let mut app = App::new(); /// /// fn spawn_player(mut commands: Commands){ - /// commands.spawn().insert(Life(10).insert(Player); + /// commands.spawn().insert(Life(10)).insert(Player); /// } /// /// fn massive_damage(mut query: Query<&mut Life>){ @@ -162,7 +172,7 @@ impl App { /// } /// } /// - /// fn kill_units(query: Query, mut commands: Commands){ + /// 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); @@ -179,7 +189,7 @@ impl App { /// app.update(); /// /// // Run a complex assertion on the world using a system - /// fn zero_life_is_dead(query: Query<&Life, Option<&Dead>>) -> bool { + /// fn zero_life_is_dead(query: Query<(&Life, Option<&Dead>)>) -> bool { /// for (life, maybe_dead) in query.iter(){ /// if life.0 == 0 { /// if maybe_dead.is_none(){ diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 9c9b05fbac066..694f0c4dded66 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -45,7 +45,7 @@ impl World { } /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` - pub fn assert_n_events(&self, n: usize) { + pub fn assert_n_events(&self, n: usize) { let events = self.get_resource::>().unwrap(); assert_eq!(events.iter_current_update_events().count(), n); From ded2267aea3c4b99c63e0018db225f09c727eadf Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 14:41:12 -0500 Subject: [PATCH 10/57] Spaces > tabs --- crates/bevy_app/src/testing_tools.rs | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index a6196b0910902..dbbd3bfc9b77f 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -23,7 +23,7 @@ impl App { /// /// // This system modifies our resource /// fn toggle_off(mut toggle: ResMut) { - /// *toggle = Toggle::Off; + /// *toggle = Toggle::Off; /// } /// /// app.insert_resource(Toggle::On).add_system(toggle_off); @@ -62,7 +62,7 @@ impl App { /// let mut app = App::new(); /// /// fn spawn_player(mut commands: Commands){ - /// commands.spawn().insert(Life(10)).insert(Player); + /// commands.spawn().insert(Life(10)).insert(Player); /// } /// /// app.add_startup_system(spawn_player); @@ -97,9 +97,9 @@ impl App { /// struct Message(String); /// /// fn print_messages(mut messages: EventReader){ - /// for message in messages.iter(){ - /// println!("{}", message.0); - /// } + /// for message in messages.iter(){ + /// println!("{}", message.0); + /// } /// } /// /// app.add_event::().add_system(print_messages); @@ -163,21 +163,21 @@ impl App { /// let mut app = App::new(); /// /// fn spawn_player(mut commands: Commands){ - /// commands.spawn().insert(Life(10)).insert(Player); + /// 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; - /// } + /// 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); - /// } - /// } + /// for (entity, life) in query.iter(){ + /// if life.0 == 0 { + /// commands.entity(entity).insert(Dead); + /// } + /// } /// } /// /// app.add_startup_system(spawn_player) @@ -190,21 +190,21 @@ impl App { /// /// // Run a complex assertion on the world using a system /// fn zero_life_is_dead(query: Query<(&Life, Option<&Dead>)>) -> bool { - /// for (life, maybe_dead) in query.iter(){ - /// if life.0 == 0 { - /// if maybe_dead.is_none(){ - /// return false; - /// } - /// } - /// - /// if maybe_dead.is_some(){ - /// if life.0 != 0 { - /// return false; - /// } - /// } - /// } + /// for (life, maybe_dead) in query.iter(){ + /// if life.0 == 0 { + /// if maybe_dead.is_none(){ + /// return false; + /// } + /// } + /// + /// if maybe_dead.is_some(){ + /// if life.0 != 0 { + /// return false; + /// } + /// } + /// } /// // None of our checks failed, so our world state is clean - /// true + /// true /// } /// /// app.update(); From 6f1c1f3f067926a79ed5390a935ebd00c41f4618 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 15:00:26 -0500 Subject: [PATCH 11/57] assert_system should use a Result --- crates/bevy_app/src/testing_tools.rs | 30 ++++++++++++++++------ crates/bevy_ecs/src/world/testing_tools.rs | 11 +++++--- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index dbbd3bfc9b77f..3cc591e3a19d9 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -141,10 +141,16 @@ impl App { self.world.assert_n_events::(n); } - /// Asserts that when the supplied `system` is run on the world, its output will be `true` + /// Asserts that when the supplied `system` is run on the world, its output will be `Ok` /// - /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". - /// Because we are generating a new system; these filters will always be true. + /// 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 @@ -188,18 +194,23 @@ impl App { /// // 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>)>) -> bool { + /// 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 false; + /// return Err(DeathError::ZeroLifeIsNotDead); /// } /// } /// /// if maybe_dead.is_some(){ /// if life.0 != 0 { - /// return false; + /// return Err(DeathError::DeadWithNonZeroLife); /// } /// } /// } @@ -208,9 +219,12 @@ impl App { /// } /// /// app.update(); - /// app.assert_system(zero_life_is_dead); + /// app.assert_system(zero_life_is_dead, None); /// ``` - pub fn assert_system(&mut self, system: impl IntoSystem<(), bool, Params>) { + pub fn assert_system( + &mut self, + system: impl IntoSystem<(), Result, SystemParams>, + ) { self.world.assert_system(system); } } diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 694f0c4dded66..e563f0aa80fd7 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -55,14 +55,17 @@ impl World { /// /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". /// Because we are generating a new system; these filters will always be true. - pub fn assert_system(&mut self, system: impl IntoSystem<(), bool, Params>) { + pub fn assert_system( + &mut self, + system: impl IntoSystem<(), Result, Params>, + ) { let mut stage = SystemStage::single_threaded(); stage.add_system(system.chain(assert_system_input_true)); stage.run(self); } } -/// A chainable system that panics if its `input` is not `true` -fn assert_system_input_true(In(result): In) { - assert!(result); +/// A chainable system that panics if its `input` is not okay +fn assert_system_input_true(In(result): In>) { + assert!(result.is_ok()); } From 100784b9ed291a99b9224a2d9912f7c7eee6d300 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 15:30:32 -0500 Subject: [PATCH 12/57] Integration testing stub --- tests/integration_testing.rs | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/integration_testing.rs diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs new file mode 100644 index 0000000000000..914a402610be6 --- /dev/null +++ b/tests/integration_testing.rs @@ -0,0 +1,46 @@ +//! Integration testing Bevy apps is surprisingly easy, +//! and is a great tool for ironing out tricky bugs or enabling refactors. +//! +//! Create new files in your root `tests` directory, and then call `cargo test` to ensure that they pass. +//! +//! You can easily reuse functionality between your tests and game by organizing your logic with plugins, +//! and then use direct methods on `App` / `World` to set up test scenarios. + +use bevy::prelude::*; + +// This plugin should be defined in your `src` folder, and exported from your project +pub struct GamePlugin; + +impl Plugin for GamePlugin { + fn build(&self, app: &mut App) { + app.add_startup_system(spawn_player) + .add_system(jump) + .add_system(gravity) + .add_system(apply_velocity) + .add_system_to_stage(CoreStage::PostUpdate, clamp_position); + } +} + +#[derive(Component)] +struct Player; + +#[derive(Component, Default)] +struct Velocity(Vec3); + +// These systems don't need to be `pub`, as they're hidden within your plugin +fn spawn_player(mut commands: Commands) { + commands + .spawn() + .insert(Player) + .insert(Transform::default()) + .insert(Velocity::default()); +} + +fn apply_velocity(query: Query<(&mut Transform, &Velocity)>) {} + +fn jump() {} + +fn gravity() {} + +/// Players should not fall through the floor +fn clamp_position() {} From 12f319cf3eb151fb2241c9154320b36fd8a4d3c0 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 16:48:25 -0500 Subject: [PATCH 13/57] Fleshed out integration test example --- tests/integration_testing.rs | 176 +++++++++++++++++++++++++++++------ 1 file changed, 150 insertions(+), 26 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 914a402610be6..b9ae5422743ac 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -6,41 +6,165 @@ //! You can easily reuse functionality between your tests and game by organizing your logic with plugins, //! and then use direct methods on `App` / `World` to set up test scenarios. -use bevy::prelude::*; +use bevy::{ + input::{ElementState, InputPlugin}, + prelude::*, +}; +use game::GamePlugin; -// This plugin should be defined in your `src` folder, and exported from your project -pub struct GamePlugin; +// This module represents the code defined in your `src` folder, and exported from your project +mod game { + pub struct GamePlugin; -impl Plugin for GamePlugin { - fn build(&self, app: &mut App) { - app.add_startup_system(spawn_player) - .add_system(jump) - .add_system(gravity) - .add_system(apply_velocity) - .add_system_to_stage(CoreStage::PostUpdate, clamp_position); + impl Plugin for GamePlugin { + fn build(&self, app: &mut App) { + app.add_startup_system(spawn_player) + .add_system(jump) + .add_system(gravity) + .add_system(apply_velocity) + .add_system_to_stage(CoreStage::PostUpdate, clamp_position); + } + } + + #[derive(Component)] + struct Player; + + #[derive(Component, Default)] + struct Velocity(Vec3); + + // These systems don't need to be `pub`, as they're hidden within your plugin + fn spawn_player(mut commands: Commands) { + commands + .spawn() + .insert(Player) + .insert(Transform::default()) + .insert(Velocity::default()); + } + + fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) { + for (mut transform, velocity) in query.iter_mut() { + transform.translation += velocity.0; + } + } + + fn jump(mut query: Query<&mut Velocity, With>, keyboard_input: Res>) { + if keyboard_input.just_pressed(KeyCode::Space) { + let mut player_velocity = query.single_mut(); + player_velocity.0.y += 10.0; + } + } + + fn gravity(mut query: Query<(&mut Velocity, &Transform)>) { + for (mut velocity, transform) in query.iter_mut() { + if transform.translation.y >= 0.0 { + velocity.0.y -= 1.0; + } + } + } + + /// Players should not fall through the floor + fn clamp_position(mut query: Query<(&mut Velocity, &mut Transform)>) { + for (mut velocity, mut transform) in query.iter() { + if transform.translation.y <= 0.0 { + velocity.0.y = 0.0; + transform.translation.y == 0.0; + } + } } } -#[derive(Component)] -struct Player; +/// A convenenience method to reduce code duplication in tests +fn test_app() -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins).add_plugin(GamePlugin); + app +} + +#[test] +fn player_falls() { + let mut app = test_app(); + + // Allowing the game to initialize, + // running all systems in the schedule once + app.update(); -#[derive(Component, Default)] -struct Velocity(Vec3); + // Moving the player up + let mut player_query = app.world.query_filtered::<&mut Transform, With>(); + let mut player_transform = player_query.single_mut(); + player_transform.translation.y = 3.0; -// These systems don't need to be `pub`, as they're hidden within your plugin -fn spawn_player(mut commands: Commands) { - commands - .spawn() - .insert(Player) - .insert(Transform::default()) - .insert(Velocity::default()); + // Running the app again + // This should cause gravity to take effect and make the player fall + app.update(); + + let mut player_query = app.world.query_filtered::<&Transform, With>(); + let player_transform = player_query.single(); + + // When possible, try to make assertions about behavior, rather than detailed outcomes + // This will help make your tests robust to irrelevant changes + assert!(player_transform.translation.y < 3.0); +} + +#[test] +fn player_does_not_fall_through_floor() { + // From the `player_falls` test, we know that gravity is working + let mut app = test_app(); + + // The player should start on the floor + app.update(); + let mut player_query = app.world.query_filtered::<&Transform, With>(); + let player_transform = player_query.single(); + assert!(player_transform.translation.y == 0.0); + + // Even after some time, the player should not fall through the floor + for _ in 0..3 { + app.update(); + } + + let mut player_query = app.world.query_filtered::<&Transform, With>(); + let player_transform = player_query.single(); + assert!(player_transform.translation.y == 0.0); + + // If we drop the player from a height, they should eventually come to rest on the floor + let mut player_query = app.world.query_filtered::<&mut Transform, With>(); + let mut player_transform = player_query.single_mut(); + player_transform.translation.y = 10.0; + + // A while later... + for _ in 0..10 { + app.update(); + } + + // The player should have landed by now + let mut player_query = app.world.query_filtered::<&Transform, With>(); + let player_transform = player_query.single(); + assert!(player_transform.translation.y == 0.0); } -fn apply_velocity(query: Query<(&mut Transform, &Velocity)>) {} +#[test] +fn jumping_moves_player_upwards() { + let mut app = test_app(); + + // We need to make sure to enable the standard input systems for this test + app.add_plugin(InputPlugin); -fn jump() {} + // Spawn everything in + app.update(); -fn gravity() {} + // Send a maximally realistic keyboard input + // In most cases, it's sufficient to just press the correct value of the `Input` resource + app.send_event(KeyBoardInput { + // The scan code represents the physical button pressed + // + // In the case of "Space", this is commonly 44. + scan_code: 44, + // The KeyCode is the "logical key" that the input represents + key_code: Some(KeyCode::Space), + state: ElementState::Pressed, + }); -/// Players should not fall through the floor -fn clamp_position() {} + // Check that the player has moved upwards due to jumping + let mut player_query = app.world.query_filtered::<&Transform, With>(); + let player_transform = player_query.single(); + assert!(player_transform.translation.y > 0.0); +} From 686526edbc54ec3800570b00b43297e1cc5c4d4c Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 17:07:56 -0500 Subject: [PATCH 14/57] assert_component_eq helper method --- crates/bevy_app/src/testing_tools.rs | 11 +++++++++++ crates/bevy_ecs/src/world/testing_tools.rs | 19 +++++++++++++++++++ tests/integration_testing.rs | 15 ++++++--------- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 3cc591e3a19d9..9e4db0e35e38d 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -1,4 +1,5 @@ use crate::App; +use bevy_ecs::component::Component; use bevy_ecs::query::{FilterFetch, WorldQuery}; use bevy_ecs::system::IntoSystem; use bevy_ecs::system::Resource; @@ -46,6 +47,16 @@ impl App { 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` + pub fn assert_component_eq(&mut self, value: &C) + where + C: Component + PartialEq + Debug, + F: WorldQuery, + ::Fetch: FilterFetch, + { + self.world.assert_component_eq::(value); + } + /// Asserts that the number of entities returned by the query is exactly `n` /// /// # Example diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index e563f0aa80fd7..0fe026b83a26d 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -2,6 +2,8 @@ //! //! 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::event::Events; use crate::schedule::{Stage, SystemStage}; use crate::system::{In, IntoChainSystem, IntoSystem}; @@ -25,6 +27,23 @@ impl World { assert_eq!(*resource, value); } + /// Asserts that all components of type `C` returned by a query with the filter `F` will equal `value` + pub fn assert_component_eq(&mut self, value: &C) + where + C: Component + PartialEq + Debug, + F: 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:?}." + ); + } + } + } + /// Asserts that the number of entities returned by the query is exactly `n` pub fn assert_n_in_query(&mut self, n: usize) where diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index b9ae5422743ac..3224bdedc561b 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -5,6 +5,9 @@ //! //! You can easily reuse functionality between your tests and game by organizing your logic with plugins, //! and then use direct methods on `App` / `World` to set up test scenarios. +//! +//! There are many helpful assertion methods on [`App`] that correspond to methods on [`World`]; +//! browse the docs to discover more! use bevy::{ input::{ElementState, InputPlugin}, @@ -112,18 +115,14 @@ fn player_does_not_fall_through_floor() { // The player should start on the floor app.update(); - let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query.single(); - assert!(player_transform.translation.y == 0.0); + app.assert_component_eq::>(Transform::from_xyz(0.0, 0.0, 0.0)); // Even after some time, the player should not fall through the floor for _ in 0..3 { app.update(); } - let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query.single(); - assert!(player_transform.translation.y == 0.0); + app.assert_component_eq::>(Transform::from_xyz(0.0, 0.0, 0.0)); // If we drop the player from a height, they should eventually come to rest on the floor let mut player_query = app.world.query_filtered::<&mut Transform, With>(); @@ -136,9 +135,7 @@ fn player_does_not_fall_through_floor() { } // The player should have landed by now - let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query.single(); - assert!(player_transform.translation.y == 0.0); + app.assert_component_eq::>(Transform::from_xyz(0.0, 0.0, 0.0)); } #[test] From 693c1f12e7ac8fb70c31838b228d9a3a53a70306 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 17:22:53 -0500 Subject: [PATCH 15/57] More docs for assert_component_eq --- crates/bevy_app/src/testing_tools.rs | 49 ++++++++++++++++++++++ crates/bevy_ecs/src/world/testing_tools.rs | 4 ++ 2 files changed, 53 insertions(+) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 9e4db0e35e38d..c8ae84dfdd015 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -48,6 +48,55 @@ impl App { } /// Asserts that all components of type `C` returned by a query with the filter `F` will equal `value` + /// + /// 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(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(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<(With, With)>(Life(10)); + /// ``` pub fn assert_component_eq(&mut self, value: &C) where C: Component + PartialEq + Debug, diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 0fe026b83a26d..33d0c4a54a583 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -28,6 +28,10 @@ impl World { } /// Asserts that all components of type `C` returned by a query with the filter `F` will equal `value` + /// + /// 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(&mut self, value: &C) where C: Component + PartialEq + Debug, From 8eb4246913b2fcfab2d361c16c259aa20f56921c Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 17:30:13 -0500 Subject: [PATCH 16/57] Replace assert_n_in_query with query_len --- crates/bevy_app/src/testing_tools.rs | 14 ++++++++------ crates/bevy_ecs/src/world/testing_tools.rs | 8 +++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index c8ae84dfdd015..c217ca2e4ae7f 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -49,6 +49,8 @@ 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`](crate::query::Changed) and [`Added`](crate::query::Added) filters /// will always return true. @@ -106,7 +108,7 @@ impl App { self.world.assert_component_eq::(value); } - /// Asserts that the number of entities returned by the query is exactly `n` + /// Returns the number of entities found by the [`Query`](bevy_ecs::system::Query) with the type parameters `Q` and `F` /// /// # Example /// ```rust @@ -126,23 +128,23 @@ impl App { /// } /// /// app.add_startup_system(spawn_player); - /// app.assert_n_in_query::<&Life, With>(0); + /// assert_eq!(app.query_len::<&Life, With>(), 0); /// /// // Run the `Schedule` once, causing our startup system to run /// app.update(); - /// app.assert_n_in_query::<&Life, With>(1); + /// assert_eq!(app.query_len::<&Life, With>(), 1); /// /// // Running the schedule again won't cause startup systems to rerun /// app.update(); - /// app.assert_n_in_query::<&Life, With>(1); + /// assert_eq!(app.query_len::<&Life, With>(), 1); /// ``` - pub fn assert_n_in_query(&mut self, n: usize) + pub fn query_len(&mut self) where Q: WorldQuery, F: WorldQuery, ::Fetch: FilterFetch, { - self.world.assert_n_in_query::(n); + self.world.query_len::(); } /// Sends an `event` of type `E` diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 33d0c4a54a583..d55fab0543619 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -29,6 +29,8 @@ 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. @@ -48,15 +50,15 @@ impl World { } } - /// Asserts that the number of entities returned by the query is exactly `n` - pub fn assert_n_in_query(&mut self, n: usize) + /// Returns the number of entities found by the [`Query`](crate::system::Query) with the type parameters `Q` and `F` + pub fn query_len(&mut self) -> usize where Q: WorldQuery, F: WorldQuery, ::Fetch: FilterFetch, { let mut query_state = self.query_filtered::(); - assert_eq!(query_state.iter(self).count(), n); + query_state.iter(self).count() } /// Sends an `event` of type `E` From 5c62969504e293606707797d0e12b831d56890bd Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 17:31:38 -0500 Subject: [PATCH 17/57] Warning about startup system footgun --- tests/integration_testing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 3224bdedc561b..d511c814915de 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -80,6 +80,8 @@ mod game { fn test_app() -> App { let mut app = App::new(); app.add_plugins(MinimalPlugins).add_plugin(GamePlugin); + // It is generally unwise to run the initial update in convenience methods like this + // as startup systems added by later plugins will be missed app } From 42849fd86f22c947f20e2b4f8da66a98f674fcdc Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 17:34:01 -0500 Subject: [PATCH 18/57] Thanks clippy --- crates/bevy_ecs/src/world/testing_tools.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index d55fab0543619..4abf76178ee9f 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -41,7 +41,7 @@ impl World { ::Fetch: FilterFetch, { let mut query_state = self.query_filtered::<(Entity, &C), F>(); - for (entity, component) in query_state.iter(&self) { + for (entity, component) in query_state.iter(self) { if component != value { panic!( "Found component {component:?} for {entity:?}, but was expecting {value:?}." From 56fee27b3eba07aaca81037f2b661f77a54ae8f9 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 17:40:51 -0500 Subject: [PATCH 19/57] assert_n_events -> events_len --- crates/bevy_app/src/testing_tools.rs | 10 +++++----- crates/bevy_ecs/src/world/testing_tools.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index c217ca2e4ae7f..05a81674d2488 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -190,17 +190,17 @@ impl App { /// /// let mut app = App::new(); /// app.add_event::(); - /// app.assert_n_events::(0); + /// assert_eq!(app.n_events::(), 0); /// /// app.send_event(SelfDestruct); - /// app.assert_n_events::(1); + /// assert_eq!(app.n_events::(), 1); /// /// // Time passes /// app.update(); - /// app.assert_n_events::(0); + /// assert_eq!(app.n_events::(), 0); /// ``` - pub fn assert_n_events(&self, n: usize) { - self.world.assert_n_events::(n); + pub fn events_len(&self) -> usize { + self.world.events_len::() } /// Asserts that when the supplied `system` is run on the world, its output will be `Ok` diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 4abf76178ee9f..8643d8842273d 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -69,11 +69,11 @@ impl World { events.send(event); } - /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` - pub fn assert_n_events(&self, n: usize) { + /// Returns the number of events of the type `E` that were sent this frame + pub fn events_len(&self) -> usize { let events = self.get_resource::>().unwrap(); - assert_eq!(events.iter_current_update_events().count(), n); + events.iter_current_update_events().count() } /// Asserts that when the supplied `system` is run on the world, its output will be `true` From 341ca093f81d94a6a822643a6b6f19593f42ac7b Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:02:57 -0500 Subject: [PATCH 20/57] Split methods, and hide assertions behind test feature flag --- crates/bevy_app/src/app.rs | 99 ++++++++++++++++++++ crates/bevy_app/src/testing_tools.rs | 100 ++------------------- crates/bevy_ecs/src/world/mod.rs | 30 +++++++ crates/bevy_ecs/src/world/testing_tools.rs | 31 +------ 4 files changed, 137 insertions(+), 123 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 6dc3ef9bcf9c2..c45b6f59aa7b3 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -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, @@ -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>(), 0); + /// + /// // Run the `Schedule` once, causing our startup system to run + /// app.update(); + /// assert_eq!(app.query_len::<&Life, With>(), 1); + /// + /// // Running the schedule again won't cause startup systems to rerun + /// app.update(); + /// assert_eq!(app.query_len::<&Life, With>(), 1); + /// ``` + pub fn query_len(&mut self) + where + Q: WorldQuery, + F: WorldQuery, + ::Fetch: FilterFetch, + { + self.world.query_len::(); + } + + /// 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){ + /// for message in messages.iter(){ + /// println!("{}", message.0); + /// } + /// } + /// + /// app.add_event::().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(&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::(); + /// assert_eq!(app.n_events::(), 0); + /// + /// app.send_event(SelfDestruct); + /// assert_eq!(app.n_events::(), 1); + /// + /// // Time passes + /// app.update(); + /// assert_eq!(app.n_events::(), 0); + /// ``` + pub fn events_len(&self) -> usize { + self.world.events_len::() + } +} + fn run_once(mut app: App) { app.update(); } diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 05a81674d2488..7aa865c2f35a9 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -1,3 +1,8 @@ +//! Tools for convenient integration testing of the ECS. +//! +//! Each of these methods has a corresponding method on `World`. +#![cfg(test)] + use crate::App; use bevy_ecs::component::Component; use bevy_ecs::query::{FilterFetch, WorldQuery}; @@ -108,101 +113,6 @@ impl App { self.world.assert_component_eq::(value); } - /// 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>(), 0); - /// - /// // Run the `Schedule` once, causing our startup system to run - /// app.update(); - /// assert_eq!(app.query_len::<&Life, With>(), 1); - /// - /// // Running the schedule again won't cause startup systems to rerun - /// app.update(); - /// assert_eq!(app.query_len::<&Life, With>(), 1); - /// ``` - pub fn query_len(&mut self) - where - Q: WorldQuery, - F: WorldQuery, - ::Fetch: FilterFetch, - { - self.world.query_len::(); - } - - /// 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){ - /// for message in messages.iter(){ - /// println!("{}", message.0); - /// } - /// } - /// - /// app.add_event::().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(&mut self, event: E) { - self.world.send_event(event); - } - - /// Asserts that the number of events of the type `E` that were sent this frame is exactly `n` - /// - /// # 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::(); - /// assert_eq!(app.n_events::(), 0); - /// - /// app.send_event(SelfDestruct); - /// assert_eq!(app.n_events::(), 1); - /// - /// // Time passes - /// app.update(); - /// assert_eq!(app.n_events::(), 0); - /// ``` - pub fn events_len(&self) -> usize { - self.world.events_len::() - } - /// 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. diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index a467e56b7a8c6..cc0cf476586ee 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -14,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, @@ -1145,6 +1146,35 @@ 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(&mut self) -> usize + where + Q: WorldQuery, + F: WorldQuery, + ::Fetch: FilterFetch, + { + let mut query_state = self.query_filtered::(); + query_state.iter(self).count() + } + + /// Sends an `event` of type `E` + pub fn send_event(&mut self, event: E) { + let mut events: Mut> = self.get_resource_mut() + .expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::()`?"); + + events.send(event); + } + + /// Returns the number of events of the type `E` that were sent this frame + pub fn events_len(&self) -> usize { + let events = self.get_resource::>().unwrap(); + + events.iter_current_update_events().count() + } +} + impl fmt::Debug for World { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("World") diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 8643d8842273d..59a7721167196 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -2,12 +2,13 @@ //! //! Each of these methods has a corresponding method on `App`; //! in many cases, these are more convenient to use. +#![cfg(test)] + use crate::component::Component; use crate::entity::Entity; -use crate::event::Events; use crate::schedule::{Stage, SystemStage}; use crate::system::{In, IntoChainSystem, IntoSystem}; -use crate::world::{FilterFetch, Mut, Resource, World, WorldQuery}; +use crate::world::{FilterFetch, Resource, World, WorldQuery}; use std::fmt::Debug; impl World { @@ -50,32 +51,6 @@ 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(&mut self) -> usize - where - Q: WorldQuery, - F: WorldQuery, - ::Fetch: FilterFetch, - { - let mut query_state = self.query_filtered::(); - query_state.iter(self).count() - } - - /// Sends an `event` of type `E` - pub fn send_event(&mut self, event: E) { - let mut events: Mut> = self.get_resource_mut() - .expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::()`?"); - - events.send(event); - } - - /// Returns the number of events of the type `E` that were sent this frame - pub fn events_len(&self) -> usize { - let events = self.get_resource::>().unwrap(); - - events.iter_current_update_events().count() - } - /// Asserts that when the supplied `system` is run on the world, its output will be `true` /// /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". From 34da21e96080c905f5a679f13007173c956d20b0 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:04:55 -0500 Subject: [PATCH 21/57] Improve error handling system name --- crates/bevy_ecs/src/world/testing_tools.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 59a7721167196..88d8517988ddd 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -60,12 +60,12 @@ impl World { system: impl IntoSystem<(), Result, Params>, ) { let mut stage = SystemStage::single_threaded(); - stage.add_system(system.chain(assert_system_input_true)); + stage.add_system(system.chain(assert_input_system_ok)); stage.run(self); } } -/// A chainable system that panics if its `input` is not okay -fn assert_system_input_true(In(result): In>) { +/// A chainable system that panics if its `input`'s [`Result`] is not okay +fn assert_input_system_ok(In(result): In>) { assert!(result.is_ok()); } From 1335372216b53ef9f35c61f9c20a443b1caf1799 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:09:52 -0500 Subject: [PATCH 22/57] Unhide assertion methods --- crates/bevy_app/src/testing_tools.rs | 1 - crates/bevy_ecs/src/world/testing_tools.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 7aa865c2f35a9..78cd6cde5e3c9 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -1,7 +1,6 @@ //! Tools for convenient integration testing of the ECS. //! //! Each of these methods has a corresponding method on `World`. -#![cfg(test)] use crate::App; use bevy_ecs::component::Component; diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 88d8517988ddd..73d4b9f091720 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -2,7 +2,6 @@ //! //! Each of these methods has a corresponding method on `App`; //! in many cases, these are more convenient to use. -#![cfg(test)] use crate::component::Component; use crate::entity::Entity; From 038ccb423c32005e5f6b6b9ccc650db44238c4a0 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:14:56 -0500 Subject: [PATCH 23/57] Spaces >> tabs --- crates/bevy_app/src/app.rs | 2 +- crates/bevy_app/src/testing_tools.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index c45b6f59aa7b3..d21866485c29b 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -991,7 +991,7 @@ impl App { /// /// // An event type /// #[derive(Debug)] - /// struct SelfDestruct; + /// struct SelfDestruct; /// /// let mut app = App::new(); /// app.add_event::(); diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 78cd6cde5e3c9..b40b24e6b044a 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -152,7 +152,7 @@ impl App { /// 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); + /// commands.entity(entity).insert(Dead); /// } /// } /// } @@ -166,8 +166,8 @@ impl App { /// app.update(); /// /// enum DeathError { - /// ZeroLifeIsNotDead, - /// DeadWithNonZeroLife, + /// ZeroLifeIsNotDead, + /// DeadWithNonZeroLife, /// } /// /// // Run a complex assertion on the world using a system @@ -185,7 +185,7 @@ impl App { /// } /// } /// } - /// // None of our checks failed, so our world state is clean + /// // None of our checks failed, so our world state is clean /// true /// } /// From 751d42bd0b90b78cf69035117e41416741157a5e Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:28:57 -0500 Subject: [PATCH 24/57] Fix missing imports --- tests/integration_testing.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index d511c814915de..a4a4c2ad72705 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -13,10 +13,12 @@ use bevy::{ input::{ElementState, InputPlugin}, prelude::*, }; -use game::GamePlugin; +use game::{GamePlugin, Player, Velocity}; // This module represents the code defined in your `src` folder, and exported from your project mod game { + use bevy::prelude::*; + pub struct GamePlugin; impl Plugin for GamePlugin { @@ -30,10 +32,10 @@ mod game { } #[derive(Component)] - struct Player; + pub struct Player; #[derive(Component, Default)] - struct Velocity(Vec3); + pub struct Velocity(Vec3); // These systems don't need to be `pub`, as they're hidden within your plugin fn spawn_player(mut commands: Commands) { From d90175d91dd8f3e2158b69a7e059ac81362326fb Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:30:55 -0500 Subject: [PATCH 25/57] , not ; Co-authored-by: KDecay --- crates/bevy_app/src/testing_tools.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index b40b24e6b044a..09d5b5cd1e0be 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -121,7 +121,7 @@ impl App { /// /// 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. + /// Because we are running a new system, these filters will always be true. /// /// # Example /// ```rust From be7e0f4221a05bb3476bc4fd2ddc971e8fd6d990 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:31:12 -0500 Subject: [PATCH 26/57] Typo fix Co-authored-by: KDecay --- crates/bevy_ecs/src/world/testing_tools.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 73d4b9f091720..159e1b05b0194 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -53,7 +53,7 @@ impl World { /// Asserts that when the supplied `system` is run on the world, its output will be `true` /// /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". - /// Because we are generating a new system; these filters will always be true. + /// Because we are generating a new system, these filters will always be true. pub fn assert_system( &mut self, system: impl IntoSystem<(), Result, Params>, From 2de9ca735e2befdb0c776b414d1645f76b439158 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:31:32 -0500 Subject: [PATCH 27/57] Typo fix Co-authored-by: KDecay --- tests/integration_testing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index a4a4c2ad72705..0241d21d2e0df 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -78,7 +78,7 @@ mod game { } } -/// A convenenience method to reduce code duplication in tests +/// A convenience method to reduce code duplication in tests fn test_app() -> App { let mut app = App::new(); app.add_plugins(MinimalPlugins).add_plugin(GamePlugin); From 2ade9ae2439aa95680e330d29282e69cf8c388d6 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:32:09 -0500 Subject: [PATCH 28/57] Typo fix --- crates/bevy_app/src/testing_tools.rs | 4 ++-- crates/bevy_ecs/src/world/testing_tools.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 09d5b5cd1e0be..98189a44993b4 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -10,7 +10,7 @@ use bevy_ecs::system::Resource; use std::fmt::Debug; impl App { - /// Asserts that that the current value of the resource `R` is `value` + /// Asserts that the current value of the resource `R` is `value` /// /// # Example /// ```rust @@ -46,7 +46,7 @@ impl App { self.world.assert_resource_eq(value); } - /// Asserts that that the current value of the non-send resource `NS` is `value` + /// Asserts that the current value of the non-send resource `NS` is `value` pub fn assert_nonsend_resource_eq(&self, value: NS) { self.world.assert_nonsend_resource_eq(value); } diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 159e1b05b0194..e96f5b6b10b1a 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -11,7 +11,7 @@ use crate::world::{FilterFetch, Resource, World, WorldQuery}; use std::fmt::Debug; impl World { - /// Asserts that that the current value of the resource `R` is `value` + /// Asserts that the current value of the resource `R` is `value` pub fn assert_resource_eq(&self, value: R) { let resource = self .get_resource::() @@ -19,7 +19,7 @@ impl World { assert_eq!(*resource, value); } - /// Asserts that that the current value of the non-send resource `NS` is `value` + /// Asserts that the current value of the non-send resource `NS` is `value` pub fn assert_nonsend_resource_eq(&self, value: NS) { let resource = self .get_non_send_resource::() From d1d740c371c25386c676c79473d948e8f7685c86 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:34:16 -0500 Subject: [PATCH 29/57] Unwrap -> expect --- crates/bevy_ecs/src/world/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index cc0cf476586ee..0bfd6d9373d23 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1169,7 +1169,8 @@ impl World { /// Returns the number of events of the type `E` that were sent this frame pub fn events_len(&self) -> usize { - let events = self.get_resource::>().unwrap(); + let events = self.get_resource::>() + .expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::()`?"); events.iter_current_update_events().count() } From 00d3aeec060961b286124f6c031f42f20fb90f5a Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:37:45 -0500 Subject: [PATCH 30/57] Fix typo --- tests/integration_testing.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 0241d21d2e0df..f6d3230fdb950 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -10,7 +10,7 @@ //! browse the docs to discover more! use bevy::{ - input::{ElementState, InputPlugin}, + input::{keyboard::KeyboardInput, ElementState, InputPlugin}, prelude::*, }; use game::{GamePlugin, Player, Velocity}; @@ -154,7 +154,7 @@ fn jumping_moves_player_upwards() { // Send a maximally realistic keyboard input // In most cases, it's sufficient to just press the correct value of the `Input` resource - app.send_event(KeyBoardInput { + app.send_event(KeyboardInput { // The scan code represents the physical button pressed // // In the case of "Space", this is commonly 44. From 9eb0418397de9077e3cef6c4a22ada0283e0e97d Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:48:02 -0500 Subject: [PATCH 31/57] Compare by reference --- crates/bevy_app/src/testing_tools.rs | 8 ++++---- crates/bevy_ecs/src/world/testing_tools.rs | 8 ++++---- tests/integration_testing.rs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 98189a44993b4..a81ccd45522b4 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -42,12 +42,12 @@ impl App { /// // Checking that our resource was modified correctly /// app.assert_resource_eq(Toggle::Off); /// ``` - pub fn assert_resource_eq(&self, value: R) { + pub fn assert_resource_eq(&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(&self, value: NS) { + pub fn assert_nonsend_resource_eq(&self, value: &NS) { self.world.assert_nonsend_resource_eq(value); } @@ -90,7 +90,7 @@ impl App { /// // and life to regenerate once /// app.update(); /// // The `()` value for `F` will result in an unfiltered query - /// app.assert_component_eq<()>(Life(9)); + /// app.assert_component_eq<()>(&Life(9)); /// /// app.update(); /// // Because all of our entities with the `Life` component also @@ -101,7 +101,7 @@ impl App { /// // 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<(With, With)>(Life(10)); + /// app.assert_component_eq<(With, With)>(&Life(10)); /// ``` pub fn assert_component_eq(&mut self, value: &C) where diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index e96f5b6b10b1a..0e3577bd271fa 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -12,19 +12,19 @@ use std::fmt::Debug; impl World { /// Asserts that the current value of the resource `R` is `value` - pub fn assert_resource_eq(&self, value: R) { + pub fn assert_resource_eq(&self, value: &R) { let resource = self .get_resource::() .expect("No resource matching the type of {value} was found in the world."); - assert_eq!(*resource, value); + assert_eq!(resource, value); } /// Asserts that the current value of the non-send resource `NS` is `value` - pub fn assert_nonsend_resource_eq(&self, value: NS) { + pub fn assert_nonsend_resource_eq(&self, value: &NS) { let resource = self .get_non_send_resource::() .expect("No non-send resource matching the type of {value} was found in the world."); - assert_eq!(*resource, value); + assert_eq!(resource, value); } /// Asserts that all components of type `C` returned by a query with the filter `F` will equal `value` diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index f6d3230fdb950..5e2be39b33612 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -119,7 +119,7 @@ fn player_does_not_fall_through_floor() { // The player should start on the floor app.update(); - app.assert_component_eq::>(Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); // Even after some time, the player should not fall through the floor for _ in 0..3 { @@ -139,7 +139,7 @@ fn player_does_not_fall_through_floor() { } // The player should have landed by now - app.assert_component_eq::>(Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); } #[test] From 4abbb002a815dc1d2b4933c049a6f7c858fbcd00 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:50:05 -0500 Subject: [PATCH 32/57] Fix generics --- crates/bevy_app/src/testing_tools.rs | 6 +++--- tests/integration_testing.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index a81ccd45522b4..702de0dee35af 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -90,18 +90,18 @@ impl App { /// // and life to regenerate once /// app.update(); /// // The `()` value for `F` will result in an unfiltered query - /// app.assert_component_eq<()>(&Life(9)); + /// app.assert_component_eq(&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(10)); + /// app.assert_component_eq>(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<(With, With)>(&Life(10)); + /// app.assert_component_eq, With)>(&Life(10)); /// ``` pub fn assert_component_eq(&mut self, value: &C) where diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 5e2be39b33612..89719e3f9fc61 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -119,14 +119,14 @@ fn player_does_not_fall_through_floor() { // The player should start on the floor app.update(); - app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); // Even after some time, the player should not fall through the floor for _ in 0..3 { app.update(); } - app.assert_component_eq::>(Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); // If we drop the player from a height, they should eventually come to rest on the floor let mut player_query = app.world.query_filtered::<&mut Transform, With>(); @@ -139,7 +139,7 @@ fn player_does_not_fall_through_floor() { } // The player should have landed by now - app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); } #[test] From 7d15c016cc6638d2e335414ef604e29b5d6c237e Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 18:55:15 -0500 Subject: [PATCH 33/57] Thank you so much clippy --- crates/bevy_app/src/testing_tools.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 702de0dee35af..adf97950243c3 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -153,7 +153,7 @@ impl App { /// for (entity, life) in query.iter(){ /// if life.0 == 0 { /// commands.entity(entity).insert(Dead); - /// } + /// } /// } /// } /// From 66a1613de2aea63dd90957debf116d893f12e8a4 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 1 Feb 2022 19:11:31 -0500 Subject: [PATCH 34/57] QueryState::single doesn't exist yet --- tests/integration_testing.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 89719e3f9fc61..02843b2494ba8 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -97,7 +97,7 @@ fn player_falls() { // Moving the player up let mut player_query = app.world.query_filtered::<&mut Transform, With>(); - let mut player_transform = player_query.single_mut(); + let mut player_transform = player_query.iter_mut(&app.world).next().unwrap(); player_transform.translation.y = 3.0; // Running the app again @@ -105,7 +105,7 @@ fn player_falls() { app.update(); let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query.single(); + let player_transform = player_query..iter().next().unwrap(); // When possible, try to make assertions about behavior, rather than detailed outcomes // This will help make your tests robust to irrelevant changes @@ -130,7 +130,7 @@ fn player_does_not_fall_through_floor() { // If we drop the player from a height, they should eventually come to rest on the floor let mut player_query = app.world.query_filtered::<&mut Transform, With>(); - let mut player_transform = player_query.single_mut(); + let mut player_transform = player_query.iter(&app.world).next().unwrap(); player_transform.translation.y = 10.0; // A while later... @@ -166,6 +166,6 @@ fn jumping_moves_player_upwards() { // Check that the player has moved upwards due to jumping let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query.single(); + let player_transform = player_query.iter(&app.world).next().unwrap(); assert!(player_transform.translation.y > 0.0); } From 0976e67dddfcdbd1f273e46cd3ff2cf5f21b81fa Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 12:58:49 -0500 Subject: [PATCH 35/57] Fix mistake in query_len method Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com> --- crates/bevy_app/src/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index d21866485c29b..b5595408abf1d 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -943,13 +943,13 @@ impl App { /// app.update(); /// assert_eq!(app.query_len::<&Life, With>(), 1); /// ``` - pub fn query_len(&mut self) + pub fn query_len(&mut self) -> usize where Q: WorldQuery, F: WorldQuery, ::Fetch: FilterFetch, { - self.world.query_len::(); + self.world.query_len::() } /// Sends an `event` of type `E` From 20a9e4c5ada33dccb9892070b6f36ad6c09a8c32 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 13:05:39 -0500 Subject: [PATCH 36/57] Integration test fixes --- tests/integration_testing.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 02843b2494ba8..275d2e708afc8 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -13,7 +13,7 @@ use bevy::{ input::{keyboard::KeyboardInput, ElementState, InputPlugin}, prelude::*, }; -use game::{GamePlugin, Player, Velocity}; +use game::{GamePlugin, Player}; // This module represents the code defined in your `src` folder, and exported from your project mod game { @@ -69,10 +69,10 @@ mod game { /// Players should not fall through the floor fn clamp_position(mut query: Query<(&mut Velocity, &mut Transform)>) { - for (mut velocity, mut transform) in query.iter() { + for (mut velocity, mut transform) in query.iter_mut() { if transform.translation.y <= 0.0 { velocity.0.y = 0.0; - transform.translation.y == 0.0; + transform.translation.y = 0.0; } } } @@ -97,7 +97,7 @@ fn player_falls() { // Moving the player up let mut player_query = app.world.query_filtered::<&mut Transform, With>(); - let mut player_transform = player_query.iter_mut(&app.world).next().unwrap(); + let mut player_transform = player_query.iter_mut(&mut app.world).next().unwrap(); player_transform.translation.y = 3.0; // Running the app again @@ -105,7 +105,7 @@ fn player_falls() { app.update(); let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query..iter().next().unwrap(); + let player_transform = player_query.iter(&mut app.world).next().unwrap(); // When possible, try to make assertions about behavior, rather than detailed outcomes // This will help make your tests robust to irrelevant changes @@ -130,7 +130,7 @@ fn player_does_not_fall_through_floor() { // If we drop the player from a height, they should eventually come to rest on the floor let mut player_query = app.world.query_filtered::<&mut Transform, With>(); - let mut player_transform = player_query.iter(&app.world).next().unwrap(); + let mut player_transform = player_query.iter_mut(&mut app.world).next().unwrap(); player_transform.translation.y = 10.0; // A while later... From 6b6a478be775e0a066ce55f58c8854a64b5026e8 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 13:31:44 -0500 Subject: [PATCH 37/57] Demonstrate assertion on a resource --- tests/integration_testing.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 275d2e708afc8..e28b77fe8edc7 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -13,7 +13,7 @@ use bevy::{ input::{keyboard::KeyboardInput, ElementState, InputPlugin}, prelude::*, }; -use game::{GamePlugin, Player}; +use game::{GamePlugin, HighestJump, Player}; // This module represents the code defined in your `src` folder, and exported from your project mod game { @@ -24,13 +24,18 @@ mod game { impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_startup_system(spawn_player) + .init_resource::() .add_system(jump) .add_system(gravity) .add_system(apply_velocity) - .add_system_to_stage(CoreStage::PostUpdate, clamp_position); + .add_system_to_stage(CoreStage::PostUpdate, clamp_position) + .add_system_to_stage(CoreStage::PostUpdate, update_highest_jump); } } + #[derive(Debug, PartialEq, Default)] + pub struct HighestJump(pub f32); + #[derive(Component)] pub struct Player; @@ -76,6 +81,16 @@ mod game { } } } + + fn update_highest_jump( + query: Query<&Transform, With>, + mut highest_jump: ResMut, + ) { + let player_transform = query.single(); + if player_transform.translation.y > highest_jump.0 { + highest_jump.0 = player_transform.translation.y; + } + } } /// A convenience method to reduce code duplication in tests @@ -110,6 +125,12 @@ fn player_falls() { // When possible, try to make assertions about behavior, rather than detailed outcomes // This will help make your tests robust to irrelevant changes assert!(player_transform.translation.y < 3.0); + assert_eq!( + app.world.get_resource::(), + Some(&HighestJump(3.0)) + ); + // FIXME: decide whether or not to keep this method + app.assert_resource_eq(&HighestJump(3.0)); } #[test] From 59ea602741d5ee1612d81cd7915f8b336f04c3bb Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 13:33:56 -0500 Subject: [PATCH 38/57] Remove unnecessary type annotation --- tests/integration_testing.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index e28b77fe8edc7..b1c8aee1a87be 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -125,10 +125,7 @@ fn player_falls() { // When possible, try to make assertions about behavior, rather than detailed outcomes // This will help make your tests robust to irrelevant changes assert!(player_transform.translation.y < 3.0); - assert_eq!( - app.world.get_resource::(), - Some(&HighestJump(3.0)) - ); + assert_eq!(app.world.get_resource(), Some(&HighestJump(3.0))); // FIXME: decide whether or not to keep this method app.assert_resource_eq(&HighestJump(3.0)); } From 1f55d662a4e8b45d6b189a335464cbfcaf8f7868 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 13:35:38 -0500 Subject: [PATCH 39/57] Remove trivial resource assertion helpers @mockersf won this round. --- crates/bevy_app/src/testing_tools.rs | 42 ---------------------- crates/bevy_ecs/src/world/testing_tools.rs | 18 +--------- tests/integration_testing.rs | 2 -- 3 files changed, 1 insertion(+), 61 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index adf97950243c3..359da29f837da 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -6,51 +6,9 @@ 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::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(&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(&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. diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 0e3577bd271fa..15eb34846bc97 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -7,26 +7,10 @@ use crate::component::Component; use crate::entity::Entity; use crate::schedule::{Stage, SystemStage}; use crate::system::{In, IntoChainSystem, IntoSystem}; -use crate::world::{FilterFetch, Resource, World, WorldQuery}; +use crate::world::{FilterFetch, World, WorldQuery}; use std::fmt::Debug; impl World { - /// Asserts that the current value of the resource `R` is `value` - pub fn assert_resource_eq(&self, value: &R) { - let resource = self - .get_resource::() - .expect("No resource matching the type of {value} was found in the world."); - assert_eq!(resource, value); - } - - /// Asserts that the current value of the non-send resource `NS` is `value` - pub fn assert_nonsend_resource_eq(&self, value: &NS) { - let resource = self - .get_non_send_resource::() - .expect("No non-send resource matching the type of {value} was found in the world."); - assert_eq!(resource, 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. diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index b1c8aee1a87be..24f93e01b8107 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -126,8 +126,6 @@ fn player_falls() { // This will help make your tests robust to irrelevant changes assert!(player_transform.translation.y < 3.0); assert_eq!(app.world.get_resource(), Some(&HighestJump(3.0))); - // FIXME: decide whether or not to keep this method - app.assert_resource_eq(&HighestJump(3.0)); } #[test] From 809c2d988bca3eb2a15130d823e01c6f19637918 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 13:36:28 -0500 Subject: [PATCH 40/57] Fix doc links --- crates/bevy_app/src/testing_tools.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 359da29f837da..c03b93d75c5ea 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -14,7 +14,7 @@ impl App { /// 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 + /// [`Changed`](bevy_ecs::query::Changed) and [`Added`](bevy_ecs::query::Added) filters /// will always return true. /// /// # Example From bcb9d1ffd596ea618dc85fd055fa072c18204a69 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 14:58:36 -0500 Subject: [PATCH 41/57] Resolve ambiguities --- tests/integration_testing.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 24f93e01b8107..f820e26115366 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -13,23 +13,32 @@ use bevy::{ input::{keyboard::KeyboardInput, ElementState, InputPlugin}, prelude::*, }; -use game::{GamePlugin, HighestJump, Player}; +use game::{HighestJump, PhysicsPlugin, Player}; // This module represents the code defined in your `src` folder, and exported from your project mod game { use bevy::prelude::*; - pub struct GamePlugin; + pub struct PhysicsPlugin; - impl Plugin for GamePlugin { + #[derive(SystemLabel, Clone, Debug, PartialEq, Eq, Hash)] + enum PhysicsLabels { + PlayerControl, + Gravity, + Velocity, + } + + impl Plugin for PhysicsPlugin { fn build(&self, app: &mut App) { + use PhysicsLabels::*; + app.add_startup_system(spawn_player) .init_resource::() - .add_system(jump) - .add_system(gravity) - .add_system(apply_velocity) + .add_system(jump.label(PlayerControl)) + .add_system(gravity.label(Gravity).after(PlayerControl)) + .add_system(apply_velocity.label(Velocity).after(Gravity)) .add_system_to_stage(CoreStage::PostUpdate, clamp_position) - .add_system_to_stage(CoreStage::PostUpdate, update_highest_jump); + .add_system_to_stage(CoreStage::PreUpdate, update_highest_jump); } } @@ -96,7 +105,9 @@ mod game { /// A convenience method to reduce code duplication in tests fn test_app() -> App { let mut app = App::new(); - app.add_plugins(MinimalPlugins).add_plugin(GamePlugin); + app.add_plugins(MinimalPlugins) + .add_plugin(PhysicsPlugin) + .add_plugin(InputPlugin); // It is generally unwise to run the initial update in convenience methods like this // as startup systems added by later plugins will be missed app @@ -162,9 +173,6 @@ fn player_does_not_fall_through_floor() { fn jumping_moves_player_upwards() { let mut app = test_app(); - // We need to make sure to enable the standard input systems for this test - app.add_plugin(InputPlugin); - // Spawn everything in app.update(); From 69a4e7a8ef6a0b75d743d187b874e8fed698860e Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 15:01:46 -0500 Subject: [PATCH 42/57] Simpler input mocking --- tests/integration_testing.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index f820e26115366..856bb49bb71ca 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -9,10 +9,7 @@ //! There are many helpful assertion methods on [`App`] that correspond to methods on [`World`]; //! browse the docs to discover more! -use bevy::{ - input::{keyboard::KeyboardInput, ElementState, InputPlugin}, - prelude::*, -}; +use bevy::{input::InputPlugin, prelude::*}; use game::{HighestJump, PhysicsPlugin, Player}; // This module represents the code defined in your `src` folder, and exported from your project @@ -176,17 +173,9 @@ fn jumping_moves_player_upwards() { // Spawn everything in app.update(); - // Send a maximally realistic keyboard input - // In most cases, it's sufficient to just press the correct value of the `Input` resource - app.send_event(KeyboardInput { - // The scan code represents the physical button pressed - // - // In the case of "Space", this is commonly 44. - scan_code: 44, - // The KeyCode is the "logical key" that the input represents - key_code: Some(KeyCode::Space), - state: ElementState::Pressed, - }); + // Send a fake keyboard press + let mut keyboard_input: Mut> = app.world.get_resource_mut().unwrap(); + keyboard_input.press(KeyCode::Space); // Check that the player has moved upwards due to jumping let mut player_query = app.world.query_filtered::<&Transform, With>(); From ba2b697db5a2bdf1cd39d06283004c3141a4ff48 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 15:26:39 -0500 Subject: [PATCH 43/57] Also assert velocity is positive --- tests/integration_testing.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 856bb49bb71ca..030dfbf1720c0 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -10,7 +10,7 @@ //! browse the docs to discover more! use bevy::{input::InputPlugin, prelude::*}; -use game::{HighestJump, PhysicsPlugin, Player}; +use game::{HighestJump, PhysicsPlugin, Player, Velocity}; // This module represents the code defined in your `src` folder, and exported from your project mod game { @@ -46,7 +46,7 @@ mod game { pub struct Player; #[derive(Component, Default)] - pub struct Velocity(Vec3); + pub struct Velocity(pub Vec3); // These systems don't need to be `pub`, as they're hidden within your plugin fn spawn_player(mut commands: Commands) { @@ -177,8 +177,16 @@ fn jumping_moves_player_upwards() { let mut keyboard_input: Mut> = app.world.get_resource_mut().unwrap(); keyboard_input.press(KeyCode::Space); + // Process the keyboard press + app.update(); + + // Check that the player has upwards velocity due to jumping + let mut player_query = app + .world + .query_filtered::<(&Velocity, &Transform), With>(); + let (player_velocity, player_transform) = player_query.iter(&app.world).next().unwrap(); + // Check that the player has moved upwards due to jumping - let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query.iter(&app.world).next().unwrap(); + assert!(player_velocity.0.y > 0.0); assert!(player_transform.translation.y > 0.0); } From 2521be2dd0d962d408d786d6fbb89293a82e106e Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 16:10:54 -0500 Subject: [PATCH 44/57] Identified why the integration test was failing --- tests/integration_testing.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 030dfbf1720c0..d3de0ec181498 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -175,11 +175,19 @@ fn jumping_moves_player_upwards() { // Send a fake keyboard press let mut keyboard_input: Mut> = app.world.get_resource_mut().unwrap(); + assert!(!keyboard_input.pressed(KeyCode::Space)); keyboard_input.press(KeyCode::Space); // Process the keyboard press app.update(); + // Verify that the input is pressed + let keyboard_input: &Input = app.world.get_resource().unwrap(); + assert!(keyboard_input.pressed(KeyCode::Space)); + // FIXME: externally sent presses will not be just_pressed + // as they are updated in `keyboard_input_system`'s call to `.clear()` + assert!(keyboard_input.just_pressed(KeyCode::Space)); + // Check that the player has upwards velocity due to jumping let mut player_query = app .world From 8a5d9e092cf65a2727446aeee3823df53efdd3a1 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 17:08:01 -0500 Subject: [PATCH 45/57] Send raw keyboard event --- tests/integration_testing.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index d3de0ec181498..9d69dd481a55c 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -168,15 +168,24 @@ fn player_does_not_fall_through_floor() { #[test] fn jumping_moves_player_upwards() { + use bevy::input::keyboard::KeyboardInput; + use bevy::input::ElementState; + let mut app = test_app(); // Spawn everything in app.update(); // Send a fake keyboard press - let mut keyboard_input: Mut> = app.world.get_resource_mut().unwrap(); - assert!(!keyboard_input.pressed(KeyCode::Space)); - keyboard_input.press(KeyCode::Space); + + // WARNING: inputs sent via pressing / releasing an Input resource + // are never just-pressed or just-released. + // Track this bug at: https://github.com/bevyengine/bevy/issues/3847 + app.send_event(KeyboardInput { + scan_code: 44, + key_code: Some(KeyCode::Space), + state: ElementState::Pressed, + }); // Process the keyboard press app.update(); @@ -184,8 +193,6 @@ fn jumping_moves_player_upwards() { // Verify that the input is pressed let keyboard_input: &Input = app.world.get_resource().unwrap(); assert!(keyboard_input.pressed(KeyCode::Space)); - // FIXME: externally sent presses will not be just_pressed - // as they are updated in `keyboard_input_system`'s call to `.clear()` assert!(keyboard_input.just_pressed(KeyCode::Space)); // Check that the player has upwards velocity due to jumping From 254ae068232ef99897fda51cb3b65fbcf14076c0 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 17:33:02 -0500 Subject: [PATCH 46/57] Clippy fix --- tests/integration_testing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 9d69dd481a55c..ce9d2bd87248a 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -128,7 +128,7 @@ fn player_falls() { app.update(); let mut player_query = app.world.query_filtered::<&Transform, With>(); - let player_transform = player_query.iter(&mut app.world).next().unwrap(); + let player_transform = player_query.iter(&app.world).next().unwrap(); // When possible, try to make assertions about behavior, rather than detailed outcomes // This will help make your tests robust to irrelevant changes From 8422cddfea4ebcdbd786407db762fb09a1744f20 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 17:34:11 -0500 Subject: [PATCH 47/57] Fix outdated doc test --- crates/bevy_app/src/app.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index b5595408abf1d..56633a45f1091 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -995,14 +995,14 @@ impl App { /// /// let mut app = App::new(); /// app.add_event::(); - /// assert_eq!(app.n_events::(), 0); + /// assert_eq!(app.events_len::(), 0); /// /// app.send_event(SelfDestruct); - /// assert_eq!(app.n_events::(), 1); + /// assert_eq!(app.events_len::(), 1); /// /// // Time passes /// app.update(); - /// assert_eq!(app.n_events::(), 0); + /// assert_eq!(app.events_len::(), 0); /// ``` pub fn events_len(&self) -> usize { self.world.events_len::() From 612d1f82e3167f48525a8b51ffcf430862d78371 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 2 Feb 2022 17:56:14 -0500 Subject: [PATCH 48/57] Fix doc tests --- crates/bevy_app/src/testing_tools.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index c03b93d75c5ea..2eb76a5436d9e 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -25,7 +25,7 @@ impl App { /// #[derive(Component)] /// struct Player; /// - /// #[derive(Component)] + /// #[derive(Component, Debug, PartialEq)] /// struct Life(usize); /// /// let mut app = App::new(); @@ -34,8 +34,8 @@ impl App { /// commands.spawn().insert(Life(8)).insert(Player); /// } /// - /// fn regenerate_life(query: Query<&Life>){ - /// for life in query.iter(){ + /// fn regenerate_life(mut query: Query<&mut Life>){ + /// for mut life in query.iter_mut(){ /// if life.0 < 10 { /// life.0 += 1; /// } @@ -48,18 +48,18 @@ impl App { /// // and life to regenerate once /// app.update(); /// // The `()` value for `F` will result in an unfiltered query - /// app.assert_component_eq(&Life(9)); + /// app.assert_component_eq::(&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(10)); + /// app.assert_component_eq::>(&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, With)>(&Life(10)); + /// app.assert_component_eq::, With)>(&Life(10)); /// ``` pub fn assert_component_eq(&mut self, value: &C) where @@ -89,7 +89,7 @@ impl App { /// #[derive(Component)] /// struct Player; /// - /// #[derive(Component)] + /// #[derive(Component, Debug, PartialEq)] /// struct Life(usize); /// /// #[derive(Component)] @@ -103,7 +103,8 @@ impl App { /// /// fn massive_damage(mut query: Query<&mut Life>){ /// for mut life in query.iter_mut(){ - /// life.0 -= 9001; + /// // Life totals can never go below zero + /// life.0 = life.0.checked_sub(9001).unwrap_or_default(); /// } /// } /// @@ -144,11 +145,11 @@ impl App { /// } /// } /// // None of our checks failed, so our world state is clean - /// true + /// Ok(()) /// } /// /// app.update(); - /// app.assert_system(zero_life_is_dead, None); + /// app.assert_system(zero_life_is_dead); /// ``` pub fn assert_system( &mut self, From 8bea4d20102f20637220abeddf20dba29de6a8af Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Fri, 4 Feb 2022 18:50:11 -0500 Subject: [PATCH 49/57] Use new events.len method --- crates/bevy_ecs/src/world/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 0bfd6d9373d23..705634e99ee70 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1172,7 +1172,7 @@ impl World { let events = self.get_resource::>() .expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::()`?"); - events.iter_current_update_events().count() + events.len() } } From 65315db19f80480eabe566888e26a19615efe89d Mon Sep 17 00:00:00 2001 From: Alice Date: Mon, 7 Feb 2022 14:09:24 -0500 Subject: [PATCH 50/57] More thoughtful events API --- crates/bevy_app/src/app.rs | 56 -------------------------------- crates/bevy_ecs/src/event.rs | 20 ++++++++++++ crates/bevy_ecs/src/world/mod.rs | 45 ++++++++++++++++--------- tests/integration_testing.rs | 2 +- 4 files changed, 50 insertions(+), 73 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 56633a45f1091..08391c0fd517b 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -951,62 +951,6 @@ impl App { { self.world.query_len::() } - - /// 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){ - /// for message in messages.iter(){ - /// println!("{}", message.0); - /// } - /// } - /// - /// app.add_event::().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(&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::(); - /// assert_eq!(app.events_len::(), 0); - /// - /// app.send_event(SelfDestruct); - /// assert_eq!(app.events_len::(), 1); - /// - /// // Time passes - /// app.update(); - /// assert_eq!(app.events_len::(), 0); - /// ``` - pub fn events_len(&self) -> usize { - self.world.events_len::() - } } fn run_once(mut app: App) { diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 5d6cb12c49b76..9615324b418a3 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -373,6 +373,26 @@ impl Events { } } + /// 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 { + 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 diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 705634e99ee70..51c3d7e573223 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -763,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::::default()); + /// + /// world.events::().send(Message("Hello World!".to_string())); + /// world.events::().send(Message("Welcome to Bevy!".to_string())); + /// + /// // Cycles the event buffer; typically automatically done once each frame + /// // using `app.add_event::()` + /// world.events::().update(); + /// + /// for event in world.events::().iter_stateless(){ + /// dbg!(event); + /// } + /// ``` + pub fn events(&mut self) -> Mut> { + self.get_resource_mut::>().expect( + "No Events 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. @@ -1158,22 +1187,6 @@ impl World { let mut query_state = self.query_filtered::(); query_state.iter(self).count() } - - /// Sends an `event` of type `E` - pub fn send_event(&mut self, event: E) { - let mut events: Mut> = self.get_resource_mut() - .expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::()`?"); - - events.send(event); - } - - /// Returns the number of events of the type `E` that were sent this frame - pub fn events_len(&self) -> usize { - let events = self.get_resource::>() - .expect("The specified event resource was not found in the world. Did you forget to call `app.add_event::()`?"); - - events.len() - } } impl fmt::Debug for World { diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index ce9d2bd87248a..5acb90266f7e9 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -181,7 +181,7 @@ fn jumping_moves_player_upwards() { // WARNING: inputs sent via pressing / releasing an Input resource // are never just-pressed or just-released. // Track this bug at: https://github.com/bevyengine/bevy/issues/3847 - app.send_event(KeyboardInput { + app.world.events().send(KeyboardInput { scan_code: 44, key_code: Some(KeyCode::Space), state: ElementState::Pressed, From dd37ec1c174606ee8642062ea03691a5b2097320 Mon Sep 17 00:00:00 2001 From: Alice Date: Mon, 7 Feb 2022 14:22:03 -0500 Subject: [PATCH 51/57] Add invariant-checking plugin to example --- tests/integration_testing.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 5acb90266f7e9..e3cb28f9d145f 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -99,12 +99,40 @@ mod game { } } +/// This plugin runs constantly in our tests, +/// and verifies that none of our internal rules have been broken. +/// +/// We can also add it to our game during development in order to proactively catch issues, +/// at the risk of sudden crashes and a small performance overhead. +/// +/// We could also handle failure more gracefully, by returning a `Result` from our systems +/// and using system chaining to log and then respond to the violation. +struct InvariantsPlugin; + +impl Plugin for InvariantsPlugin { + fn build(&self, app: &mut App) { + // Generally, assertions about invariants should be checked + // at the end or beginning of the frame, where we are "guaranteed" to have a clean state. + app.add_system_to_stage(CoreStage::Last, assert_player_does_not_fall_through_floor); + } +} + +fn assert_player_does_not_fall_through_floor(query: Query<&Transform, With>) { + // Note that query.single() also enforces an invariant: there is always exactly one Player + let player_transform = query.single(); + assert!(player_transform.translation.y >= 0.0); +} + /// A convenience method to reduce code duplication in tests fn test_app() -> App { let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugin(PhysicsPlugin) - .add_plugin(InputPlugin); + .add_plugin(InputPlugin) + // By adding this invariant-checking plugin to our test setup, + // we can automatically check for common or complex failure modes, + // without having to predict exactly when they might occur + .add_plugin(InvariantsPlugin); // It is generally unwise to run the initial update in convenience methods like this // as startup systems added by later plugins will be missed app From 3718e0007ae37b4e9227660b8fb7d1d1bb79cd68 Mon Sep 17 00:00:00 2001 From: Alice Date: Mon, 7 Feb 2022 14:30:09 -0500 Subject: [PATCH 52/57] Remove assert_system --- crates/bevy_app/src/testing_tools.rs | 89 ---------------------- crates/bevy_ecs/src/world/testing_tools.rs | 20 ----- 2 files changed, 109 deletions(-) diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs index 2eb76a5436d9e..508ae82c98faa 100644 --- a/crates/bevy_app/src/testing_tools.rs +++ b/crates/bevy_app/src/testing_tools.rs @@ -5,7 +5,6 @@ use crate::App; use bevy_ecs::component::Component; use bevy_ecs::query::{FilterFetch, WorldQuery}; -use bevy_ecs::system::IntoSystem; use std::fmt::Debug; impl App { @@ -69,92 +68,4 @@ impl App { { self.world.assert_component_eq::(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, Debug, PartialEq)] - /// 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 totals can never go below zero - /// life.0 = life.0.checked_sub(9001).unwrap_or_default(); - /// } - /// } - /// - /// 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); - /// } - /// } - /// - /// if maybe_dead.is_some(){ - /// if life.0 != 0 { - /// return Err(DeathError::DeadWithNonZeroLife); - /// } - /// } - /// } - /// // None of our checks failed, so our world state is clean - /// Ok(()) - /// } - /// - /// app.update(); - /// app.assert_system(zero_life_is_dead); - /// ``` - pub fn assert_system( - &mut self, - system: impl IntoSystem<(), Result, SystemParams>, - ) { - self.world.assert_system(system); - } } diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs index 15eb34846bc97..170a0975c46ab 100644 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -5,8 +5,6 @@ use crate::component::Component; use crate::entity::Entity; -use crate::schedule::{Stage, SystemStage}; -use crate::system::{In, IntoChainSystem, IntoSystem}; use crate::world::{FilterFetch, World, WorldQuery}; use std::fmt::Debug; @@ -33,22 +31,4 @@ impl World { } } } - - /// Asserts that when the supplied `system` is run on the world, its output will be `true` - /// - /// WARNING: [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters are computed relative to "the last time this system ran". - /// Because we are generating a new system, these filters will always be true. - pub fn assert_system( - &mut self, - system: impl IntoSystem<(), Result, Params>, - ) { - let mut stage = SystemStage::single_threaded(); - stage.add_system(system.chain(assert_input_system_ok)); - stage.run(self); - } -} - -/// A chainable system that panics if its `input`'s [`Result`] is not okay -fn assert_input_system_ok(In(result): In>) { - assert!(result.is_ok()); } From f24149277f044027d0c3cd6740b1f6f20a760a16 Mon Sep 17 00:00:00 2001 From: Alice Date: Mon, 7 Feb 2022 14:32:51 -0500 Subject: [PATCH 53/57] Remove assert_compont_eq --- crates/bevy_app/src/lib.rs | 1 - crates/bevy_app/src/testing_tools.rs | 71 ---------------------- crates/bevy_ecs/src/world/mod.rs | 1 - crates/bevy_ecs/src/world/testing_tools.rs | 34 ----------- tests/integration_testing.rs | 12 +++- 5 files changed, 9 insertions(+), 110 deletions(-) delete mode 100644 crates/bevy_app/src/testing_tools.rs delete mode 100644 crates/bevy_ecs/src/world/testing_tools.rs diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 6dc9e850978c9..a891c5957b786 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -6,7 +6,6 @@ mod app; mod plugin; mod plugin_group; mod schedule_runner; -mod testing_tools; #[cfg(feature = "bevy_ci_testing")] mod ci_testing; diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs deleted file mode 100644 index 508ae82c98faa..0000000000000 --- a/crates/bevy_app/src/testing_tools.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! 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(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(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::, With)>(&Life(10)); - /// ``` - pub fn assert_component_eq(&mut self, value: &C) - where - C: Component + PartialEq + Debug, - F: WorldQuery, - ::Fetch: FilterFetch, - { - self.world.assert_component_eq::(value); - } -} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 51c3d7e573223..1935fc75041fd 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,6 +1,5 @@ mod entity_ref; mod spawn_batch; -mod testing_tools; mod world_cell; pub use crate::change_detection::Mut; diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs deleted file mode 100644 index 170a0975c46ab..0000000000000 --- a/crates/bevy_ecs/src/world/testing_tools.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! 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(&mut self, value: &C) - where - C: Component + PartialEq + Debug, - F: 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:?}." - ); - } - } - } -} diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index e3cb28f9d145f..4453b4fbcfce3 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -171,14 +171,18 @@ fn player_does_not_fall_through_floor() { // The player should start on the floor app.update(); - app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); + let mut query_state = app.world.query_filtered::<&Transform, With>(); + let player_transform = query_state.iter(&app.world).next().unwrap(); + assert_eq!(player_transform, &Transform::from_xyz(0.0, 0.0, 0.0)); // Even after some time, the player should not fall through the floor for _ in 0..3 { app.update(); } - app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); + let mut query_state = app.world.query_filtered::<&Transform, With>(); + let player_transform = query_state.iter(&app.world).next().unwrap(); + assert_eq!(player_transform, &Transform::from_xyz(0.0, 0.0, 0.0)); // If we drop the player from a height, they should eventually come to rest on the floor let mut player_query = app.world.query_filtered::<&mut Transform, With>(); @@ -191,7 +195,9 @@ fn player_does_not_fall_through_floor() { } // The player should have landed by now - app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); + let mut query_state = app.world.query_filtered::<&Transform, With>(); + let player_transform = query_state.iter(&app.world).next().unwrap(); + assert_eq!(player_transform, &Transform::from_xyz(0.0, 0.0, 0.0)); } #[test] From 71b5fd4b2a341661b5ceb53da3be31dddc8b2a42 Mon Sep 17 00:00:00 2001 From: Alice Date: Mon, 7 Feb 2022 15:14:34 -0500 Subject: [PATCH 54/57] Failed attempt at a stateless query helper --- crates/bevy_ecs/src/world/mod.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 1935fc75041fd..b4eca589e05b6 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -16,7 +16,7 @@ use crate::{ event::Events, query::{FilterFetch, QueryState, WorldQuery}, storage::{Column, SparseSet, Storages}, - system::Resource, + system::{Query, Resource}, }; use std::{ any::TypeId, @@ -569,6 +569,27 @@ impl World { QueryState::new(self) } + /// Returns a [`Query`] for the given [`WorldQuery`] + /// + /// To access a [`Query`] at the same time as other parts of the [`World`], + /// consider using [`SystemState`](crate::system::SystemState) instead. + /// + /// ## Warning + /// This method is primarily intended for integration testing purposes. + /// No state is cached, or provided to be cached, which means: + /// 1. Overhead will be measurably higher. + /// 2. [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) query filters will be true for all compoenents. + #[inline] + pub fn query_stateless(&mut self) -> Query + where + F::Fetch: FilterFetch, + { + let query_state = QueryState::new(self); + + // SAFE: we have unique mutable access to the world + unsafe { Query::new(self, &query_state, self.change_tick(), self.change_tick()) } + } + /// Returns an iterator of entities that had components of type `T` removed /// since the last call to [`World::clear_trackers`]. pub fn removed(&self) -> std::iter::Cloned> { From b19adf43ff2ae85a19c0d04c1e1a308945312a21 Mon Sep 17 00:00:00 2001 From: Alice Date: Mon, 7 Feb 2022 15:14:34 -0500 Subject: [PATCH 55/57] Revert "Failed attempt at a stateless query helper" This reverts commit 71b5fd4b2a341661b5ceb53da3be31dddc8b2a42. --- crates/bevy_ecs/src/world/mod.rs | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index b4eca589e05b6..1935fc75041fd 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -16,7 +16,7 @@ use crate::{ event::Events, query::{FilterFetch, QueryState, WorldQuery}, storage::{Column, SparseSet, Storages}, - system::{Query, Resource}, + system::Resource, }; use std::{ any::TypeId, @@ -569,27 +569,6 @@ impl World { QueryState::new(self) } - /// Returns a [`Query`] for the given [`WorldQuery`] - /// - /// To access a [`Query`] at the same time as other parts of the [`World`], - /// consider using [`SystemState`](crate::system::SystemState) instead. - /// - /// ## Warning - /// This method is primarily intended for integration testing purposes. - /// No state is cached, or provided to be cached, which means: - /// 1. Overhead will be measurably higher. - /// 2. [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) query filters will be true for all compoenents. - #[inline] - pub fn query_stateless(&mut self) -> Query - where - F::Fetch: FilterFetch, - { - let query_state = QueryState::new(self); - - // SAFE: we have unique mutable access to the world - unsafe { Query::new(self, &query_state, self.change_tick(), self.change_tick()) } - } - /// Returns an iterator of entities that had components of type `T` removed /// since the last call to [`World::clear_trackers`]. pub fn removed(&self) -> std::iter::Cloned> { From c3f0e5a48c14a3fb5bbbf0225000ad2d6b098944 Mon Sep 17 00:00:00 2001 From: Alice Date: Mon, 7 Feb 2022 14:32:51 -0500 Subject: [PATCH 56/57] Revert "Remove assert_compont_eq" This reverts commit f24149277f044027d0c3cd6740b1f6f20a760a16. --- crates/bevy_app/src/lib.rs | 1 + crates/bevy_app/src/testing_tools.rs | 71 ++++++++++++++++++++++ crates/bevy_ecs/src/world/mod.rs | 1 + crates/bevy_ecs/src/world/testing_tools.rs | 34 +++++++++++ tests/integration_testing.rs | 12 +--- 5 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 crates/bevy_app/src/testing_tools.rs create mode 100644 crates/bevy_ecs/src/world/testing_tools.rs diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index a891c5957b786..6dc9e850978c9 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -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; diff --git a/crates/bevy_app/src/testing_tools.rs b/crates/bevy_app/src/testing_tools.rs new file mode 100644 index 0000000000000..508ae82c98faa --- /dev/null +++ b/crates/bevy_app/src/testing_tools.rs @@ -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(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(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::, With)>(&Life(10)); + /// ``` + pub fn assert_component_eq(&mut self, value: &C) + where + C: Component + PartialEq + Debug, + F: WorldQuery, + ::Fetch: FilterFetch, + { + self.world.assert_component_eq::(value); + } +} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 1935fc75041fd..51c3d7e573223 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,5 +1,6 @@ mod entity_ref; mod spawn_batch; +mod testing_tools; mod world_cell; pub use crate::change_detection::Mut; diff --git a/crates/bevy_ecs/src/world/testing_tools.rs b/crates/bevy_ecs/src/world/testing_tools.rs new file mode 100644 index 0000000000000..170a0975c46ab --- /dev/null +++ b/crates/bevy_ecs/src/world/testing_tools.rs @@ -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(&mut self, value: &C) + where + C: Component + PartialEq + Debug, + F: 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:?}." + ); + } + } + } +} diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index 4453b4fbcfce3..e3cb28f9d145f 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -171,18 +171,14 @@ fn player_does_not_fall_through_floor() { // The player should start on the floor app.update(); - let mut query_state = app.world.query_filtered::<&Transform, With>(); - let player_transform = query_state.iter(&app.world).next().unwrap(); - assert_eq!(player_transform, &Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); // Even after some time, the player should not fall through the floor for _ in 0..3 { app.update(); } - let mut query_state = app.world.query_filtered::<&Transform, With>(); - let player_transform = query_state.iter(&app.world).next().unwrap(); - assert_eq!(player_transform, &Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); // If we drop the player from a height, they should eventually come to rest on the floor let mut player_query = app.world.query_filtered::<&mut Transform, With>(); @@ -195,9 +191,7 @@ fn player_does_not_fall_through_floor() { } // The player should have landed by now - let mut query_state = app.world.query_filtered::<&Transform, With>(); - let player_transform = query_state.iter(&app.world).next().unwrap(); - assert_eq!(player_transform, &Transform::from_xyz(0.0, 0.0, 0.0)); + app.assert_component_eq::>(&Transform::from_xyz(0.0, 0.0, 0.0)); } #[test] From bd2707c62769f52010082488544cca074e0a40e2 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 7 Feb 2022 17:48:30 -0500 Subject: [PATCH 57/57] Use delta-time physics --- tests/integration_testing.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_testing.rs b/tests/integration_testing.rs index e3cb28f9d145f..a9faaf6ad13c5 100644 --- a/tests/integration_testing.rs +++ b/tests/integration_testing.rs @@ -57,9 +57,9 @@ mod game { .insert(Velocity::default()); } - fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) { + fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res