diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 6dc3ef9bcf9c2..08391c0fd517b 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,48 @@ impl App { } } +// Testing adjacents tools +impl App { + /// Returns the number of entities found by the [`Query`](bevy_ecs::system::Query) with the type parameters `Q` and `F` + /// + /// # Example + /// ```rust + /// # use bevy_app::App; + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(Component)] + /// struct Player; + /// + /// #[derive(Component)] + /// struct Life(usize); + /// + /// let mut app = App::new(); + /// + /// fn spawn_player(mut commands: Commands){ + /// commands.spawn().insert(Life(10)).insert(Player); + /// } + /// + /// app.add_startup_system(spawn_player); + /// assert_eq!(app.query_len::<&Life, With>(), 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) -> usize + where + Q: WorldQuery, + F: WorldQuery, + ::Fetch: FilterFetch, + { + self.world.query_len::() + } +} + fn run_once(mut app: App) { app.update(); } 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/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 c07892a35243a..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; @@ -13,6 +14,7 @@ use crate::{ change_detection::Ticks, component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{AllocAtWithoutReplacement, Entities, Entity}, + event::Events, query::{FilterFetch, QueryState, WorldQuery}, storage::{Column, SparseSet, Storages}, system::Resource, @@ -761,6 +763,35 @@ impl World { self.get_non_send_unchecked_mut_with_id(component_id) } + /// Get a mutable smart pointer to the [`Events`] [`Resource`] corresponding to type `E` + /// + /// ```rust + /// use bevy_ecs::world::World; + /// use bevy_ecs::event::Events; + /// + /// #[derive(Debug)] + /// struct Message(String); + /// + /// let mut world = World::new(); + /// world.insert_resource(Events::::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. @@ -1144,6 +1175,20 @@ impl World { } } +// Testing-adjacent tools +impl World { + /// Returns the number of entities found by the [`Query`](crate::system::Query) with the type parameters `Q` and `F` + pub fn query_len(&mut self) -> usize + where + Q: WorldQuery, + F: WorldQuery, + ::Fetch: FilterFetch, + { + let mut query_state = self.query_filtered::(); + query_state.iter(self).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 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 new file mode 100644 index 0000000000000..a9faaf6ad13c5 --- /dev/null +++ b/tests/integration_testing.rs @@ -0,0 +1,235 @@ +//! 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. +//! +//! There are many helpful assertion methods on [`App`] that correspond to methods on [`World`]; +//! browse the docs to discover more! + +use bevy::{input::InputPlugin, prelude::*}; +use game::{HighestJump, PhysicsPlugin, Player, Velocity}; + +// This module represents the code defined in your `src` folder, and exported from your project +mod game { + use bevy::prelude::*; + + pub struct PhysicsPlugin; + + #[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.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::PreUpdate, update_highest_jump); + } + } + + #[derive(Debug, PartialEq, Default)] + pub struct HighestJump(pub f32); + + #[derive(Component)] + pub struct Player; + + #[derive(Component, Default)] + 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) { + commands + .spawn() + .insert(Player) + .insert(Transform::default()) + .insert(Velocity::default()); + } + + fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res