bevy/crates/bevy_ecs/src/schedule/mod.rs
Joona Aalto 7b1c9f192e
Adopt consistent FooSystems naming convention for system sets (#18900)
# Objective

Fixes a part of #14274.

Bevy has an incredibly inconsistent naming convention for its system
sets, both internally and across the ecosystem.

<img alt="System sets in Bevy"
src="https://github.com/user-attachments/assets/d16e2027-793f-4ba4-9cc9-e780b14a5a1b"
width="450" />

*Names of public system set types in Bevy*

Most Bevy types use a naming of `FooSystem` or just `Foo`, but there are
also a few `FooSystems` and `FooSet` types. In ecosystem crates on the
other hand, `FooSet` is perhaps the most commonly used name in general.
Conventions being so wildly inconsistent can make it harder for users to
pick names for their own types, to search for system sets on docs.rs, or
to even discern which types *are* system sets.

To reign in the inconsistency a bit and help unify the ecosystem, it
would be good to establish a common recommended naming convention for
system sets in Bevy itself, similar to how plugins are commonly suffixed
with `Plugin` (ex: `TimePlugin`). By adopting a consistent naming
convention in first-party Bevy, we can softly nudge ecosystem crates to
follow suit (for types where it makes sense to do so).

Choosing a naming convention is also relevant now, as the [`bevy_cli`
recently adopted
lints](https://github.com/TheBevyFlock/bevy_cli/pull/345) to enforce
naming for plugins and system sets, and the recommended naming used for
system sets is still a bit open.

## Which Name To Use?

Now the contentious part: what naming convention should we actually
adopt?

This was discussed on the Bevy Discord at the end of last year, starting
[here](<https://discord.com/channels/691052431525675048/692572690833473578/1310659954683936789>).
`FooSet` and `FooSystems` were the clear favorites, with `FooSet` very
narrowly winning an unofficial poll. However, it seems to me like the
consensus was broadly moving towards `FooSystems` at the end and after
the poll, with Cart
([source](https://discord.com/channels/691052431525675048/692572690833473578/1311140204974706708))
and later Alice
([source](https://discord.com/channels/691052431525675048/692572690833473578/1311092530732859533))
and also me being in favor of it.

Let's do a quick pros and cons list! Of course these are just what I
thought of, so take it with a grain of salt.

`FooSet`:

- Pro: Nice and short!
- Pro: Used by many ecosystem crates.
- Pro: The `Set` suffix comes directly from the trait name `SystemSet`.
- Pro: Pairs nicely with existing APIs like `in_set` and
`configure_sets`.
- Con: `Set` by itself doesn't actually indicate that it's related to
systems *at all*, apart from the implemented trait. A set of what?
- Con: Is `FooSet` a set of `Foo`s or a system set related to `Foo`? Ex:
`ContactSet`, `MeshSet`, `EnemySet`...

`FooSystems`:

- Pro: Very clearly indicates that the type represents a collection of
systems. The actual core concept, system(s), is in the name.
- Pro: Parallels nicely with `FooPlugins` for plugin groups.
- Pro: Low risk of conflicts with other names or misunderstandings about
what the type is.
- Pro: In most cases, reads *very* nicely and clearly. Ex:
`PhysicsSystems` and `AnimationSystems` as opposed to `PhysicsSet` and
`AnimationSet`.
- Pro: Easy to search for on docs.rs.
- Con: Usually results in longer names.
- Con: Not yet as widely used.

Really the big problem with `FooSet` is that it doesn't actually
describe what it is. It describes what *kind of thing* it is (a set of
something), but not *what it is a set of*, unless you know the type or
check its docs or implemented traits. `FooSystems` on the other hand is
much more self-descriptive in this regard, at the cost of being a bit
longer to type.

Ultimately, in some ways it comes down to preference and how you think
of system sets. Personally, I was originally in favor of `FooSet`, but
have been increasingly on the side of `FooSystems`, especially after
seeing what the new names would actually look like in Avian and now
Bevy. I prefer it because it usually reads better, is much more clearly
related to groups of systems than `FooSet`, and overall *feels* more
correct and natural to me in the long term.

For these reasons, and because Alice and Cart also seemed to share a
preference for it when it was previously being discussed, I propose that
we adopt a `FooSystems` naming convention where applicable.

## Solution

Rename Bevy's system set types to use a consistent `FooSet` naming where
applicable.

- `AccessibilitySystem` → `AccessibilitySystems`
- `GizmoRenderSystem` → `GizmoRenderSystems`
- `PickSet` → `PickingSystems`
- `RunFixedMainLoopSystem` → `RunFixedMainLoopSystems`
- `TransformSystem` → `TransformSystems`
- `RemoteSet` → `RemoteSystems`
- `RenderSet` → `RenderSystems`
- `SpriteSystem` → `SpriteSystems`
- `StateTransitionSteps` → `StateTransitionSystems`
- `RenderUiSystem` → `RenderUiSystems`
- `UiSystem` → `UiSystems`
- `Animation` → `AnimationSystems`
- `AssetEvents` → `AssetEventSystems`
- `TrackAssets` → `AssetTrackingSystems`
- `UpdateGizmoMeshes` → `GizmoMeshSystems`
- `InputSystem` → `InputSystems`
- `InputFocusSet` → `InputFocusSystems`
- `ExtractMaterialsSet` → `MaterialExtractionSystems`
- `ExtractMeshesSet` → `MeshExtractionSystems`
- `RumbleSystem` → `RumbleSystems`
- `CameraUpdateSystem` → `CameraUpdateSystems`
- `ExtractAssetsSet` → `AssetExtractionSystems`
- `Update2dText` → `Text2dUpdateSystems`
- `TimeSystem` → `TimeSystems`
- `AudioPlaySet` → `AudioPlaybackSystems`
- `SendEvents` → `EventSenderSystems`
- `EventUpdates` → `EventUpdateSystems`

A lot of the names got slightly longer, but they are also a lot more
consistent, and in my opinion the majority of them read much better. For
a few of the names I took the liberty of rewording things a bit;
definitely open to any further naming improvements.

There are still also cases where the `FooSystems` naming doesn't really
make sense, and those I left alone. This primarily includes system sets
like `Interned<dyn SystemSet>`, `EnterSchedules<S>`, `ExitSchedules<S>`,
or `TransitionSchedules<S>`, where the type has some special purpose and
semantics.

## Todo

- [x] Should I keep all the old names as deprecated type aliases? I can
do this, but to avoid wasting work I'd prefer to first reach consensus
on whether these renames are even desired.
- [x] Migration guide
- [x] Release notes
2025-05-06 15:18:03 +00:00

1259 lines
42 KiB
Rust

//! Contains APIs for ordering systems and executing them on a [`World`](crate::world::World)
mod auto_insert_apply_deferred;
mod condition;
mod config;
mod executor;
mod pass;
mod schedule;
mod set;
mod stepping;
use self::graph::*;
pub use self::{condition::*, config::*, executor::*, schedule::*, set::*};
pub use pass::ScheduleBuildPass;
pub use self::graph::NodeId;
/// An implementation of a graph data structure.
pub mod graph;
/// Included optional schedule build passes.
pub mod passes {
pub use crate::schedule::auto_insert_apply_deferred::*;
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{string::ToString, vec, vec::Vec};
use core::sync::atomic::{AtomicU32, Ordering};
pub use crate::{
prelude::World,
resource::Resource,
schedule::{Schedule, SystemSet},
system::{Res, ResMut},
};
#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)]
enum TestSystems {
A,
B,
C,
D,
X,
}
#[derive(Resource, Default)]
struct SystemOrder(Vec<u32>);
#[derive(Resource, Default)]
struct RunConditionBool(pub bool);
#[derive(Resource, Default)]
struct Counter(pub AtomicU32);
fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) {
move |world| world.resource_mut::<SystemOrder>().0.push(tag)
}
fn make_function_system(tag: u32) -> impl FnMut(ResMut<SystemOrder>) {
move |mut resource: ResMut<SystemOrder>| resource.0.push(tag)
}
fn named_system(mut resource: ResMut<SystemOrder>) {
resource.0.push(u32::MAX);
}
fn named_exclusive_system(world: &mut World) {
world.resource_mut::<SystemOrder>().0.push(u32::MAX);
}
fn counting_system(counter: Res<Counter>) {
counter.0.fetch_add(1, Ordering::Relaxed);
}
mod system_execution {
use super::*;
#[test]
fn run_system() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems(make_function_system(0));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![0]);
}
#[test]
fn run_exclusive_system() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems(make_exclusive_system(0));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![0]);
}
#[test]
#[cfg(not(miri))]
fn parallel_execution() {
use alloc::sync::Arc;
use bevy_tasks::{ComputeTaskPool, TaskPool};
use std::sync::Barrier;
let mut world = World::default();
let mut schedule = Schedule::default();
let thread_count = ComputeTaskPool::get_or_init(TaskPool::default).thread_num();
let barrier = Arc::new(Barrier::new(thread_count));
for _ in 0..thread_count {
let inner = barrier.clone();
schedule.add_systems(move || {
inner.wait();
});
}
schedule.run(&mut world);
}
}
mod system_ordering {
use super::*;
#[test]
fn order_systems() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems((
named_system,
make_function_system(1).before(named_system),
make_function_system(0)
.after(named_system)
.in_set(TestSystems::A),
));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![1, u32::MAX, 0]);
world.insert_resource(SystemOrder::default());
assert_eq!(world.resource::<SystemOrder>().0, vec![]);
// modify the schedule after it's been initialized and test ordering with sets
schedule.configure_sets(TestSystems::A.after(named_system));
schedule.add_systems((
make_function_system(3)
.before(TestSystems::A)
.after(named_system),
make_function_system(4).after(TestSystems::A),
));
schedule.run(&mut world);
assert_eq!(
world.resource::<SystemOrder>().0,
vec![1, u32::MAX, 3, 0, 4]
);
}
#[test]
fn order_exclusive_systems() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems((
named_exclusive_system,
make_exclusive_system(1).before(named_exclusive_system),
make_exclusive_system(0).after(named_exclusive_system),
));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![1, u32::MAX, 0]);
}
#[test]
fn add_systems_correct_order() {
let mut world = World::new();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems(
(
make_function_system(0),
make_function_system(1),
make_exclusive_system(2),
make_function_system(3),
)
.chain(),
);
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![0, 1, 2, 3]);
}
#[test]
fn add_systems_correct_order_nested() {
let mut world = World::new();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems(
(
(make_function_system(0), make_function_system(1)).chain(),
make_function_system(2),
(make_function_system(3), make_function_system(4)).chain(),
(
make_function_system(5),
(make_function_system(6), make_function_system(7)),
),
(
(make_function_system(8), make_function_system(9)).chain(),
make_function_system(10),
),
)
.chain(),
);
schedule.run(&mut world);
let order = &world.resource::<SystemOrder>().0;
assert_eq!(
&order[0..5],
&[0, 1, 2, 3, 4],
"first five items should be exactly ordered"
);
let unordered = &order[5..8];
assert!(
unordered.contains(&5) && unordered.contains(&6) && unordered.contains(&7),
"unordered must be 5, 6, and 7 in any order"
);
let partially_ordered = &order[8..11];
assert!(
partially_ordered == [8, 9, 10] || partially_ordered == [10, 8, 9],
"partially_ordered must be [8, 9, 10] or [10, 8, 9]"
);
assert_eq!(order.len(), 11, "must have exactly 11 order entries");
}
}
mod conditions {
use crate::change_detection::DetectChanges;
use super::*;
#[test]
fn system_with_condition() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<RunConditionBool>();
world.init_resource::<SystemOrder>();
schedule.add_systems(
make_function_system(0).run_if(|condition: Res<RunConditionBool>| condition.0),
);
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);
world.resource_mut::<RunConditionBool>().0 = true;
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![0]);
}
#[test]
fn systems_with_distributive_condition() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.insert_resource(RunConditionBool(true));
world.init_resource::<SystemOrder>();
fn change_condition(mut condition: ResMut<RunConditionBool>) {
condition.0 = false;
}
schedule.add_systems(
(
make_function_system(0),
change_condition,
make_function_system(1),
)
.chain()
.distributive_run_if(|condition: Res<RunConditionBool>| condition.0),
);
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![0]);
}
#[test]
fn run_exclusive_system_with_condition() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<RunConditionBool>();
world.init_resource::<SystemOrder>();
schedule.add_systems(
make_exclusive_system(0).run_if(|condition: Res<RunConditionBool>| condition.0),
);
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);
world.resource_mut::<RunConditionBool>().0 = true;
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![0]);
}
#[test]
fn multiple_conditions_on_system() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<Counter>();
schedule.add_systems((
counting_system.run_if(|| false).run_if(|| false),
counting_system.run_if(|| true).run_if(|| false),
counting_system.run_if(|| false).run_if(|| true),
counting_system.run_if(|| true).run_if(|| true),
));
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
}
#[test]
fn multiple_conditions_on_system_sets() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<Counter>();
schedule.configure_sets(TestSystems::A.run_if(|| false).run_if(|| false));
schedule.add_systems(counting_system.in_set(TestSystems::A));
schedule.configure_sets(TestSystems::B.run_if(|| true).run_if(|| false));
schedule.add_systems(counting_system.in_set(TestSystems::B));
schedule.configure_sets(TestSystems::C.run_if(|| false).run_if(|| true));
schedule.add_systems(counting_system.in_set(TestSystems::C));
schedule.configure_sets(TestSystems::D.run_if(|| true).run_if(|| true));
schedule.add_systems(counting_system.in_set(TestSystems::D));
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
}
#[test]
fn systems_nested_in_system_sets() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<Counter>();
schedule.configure_sets(TestSystems::A.run_if(|| false));
schedule.add_systems(counting_system.in_set(TestSystems::A).run_if(|| false));
schedule.configure_sets(TestSystems::B.run_if(|| true));
schedule.add_systems(counting_system.in_set(TestSystems::B).run_if(|| false));
schedule.configure_sets(TestSystems::C.run_if(|| false));
schedule.add_systems(counting_system.in_set(TestSystems::C).run_if(|| true));
schedule.configure_sets(TestSystems::D.run_if(|| true));
schedule.add_systems(counting_system.in_set(TestSystems::D).run_if(|| true));
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
}
#[test]
fn system_conditions_and_change_detection() {
#[derive(Resource, Default)]
struct Bool2(pub bool);
let mut world = World::default();
world.init_resource::<Counter>();
world.init_resource::<RunConditionBool>();
world.init_resource::<Bool2>();
let mut schedule = Schedule::default();
schedule.add_systems(
counting_system
.run_if(|res1: Res<RunConditionBool>| res1.is_changed())
.run_if(|res2: Res<Bool2>| res2.is_changed()),
);
// both resource were just added.
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// nothing has changed
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// RunConditionBool has changed, but counting_system did not run
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// internal state for the bool2 run criteria was updated in the
// previous run, so system still does not run
world.get_resource_mut::<Bool2>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// internal state for bool2 was updated, so system still does not run
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// now check that it works correctly changing Bool2 first and then RunConditionBool
world.get_resource_mut::<Bool2>().unwrap().0 = false;
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 2);
}
#[test]
fn system_set_conditions_and_change_detection() {
#[derive(Resource, Default)]
struct Bool2(pub bool);
let mut world = World::default();
world.init_resource::<Counter>();
world.init_resource::<RunConditionBool>();
world.init_resource::<Bool2>();
let mut schedule = Schedule::default();
schedule.configure_sets(
TestSystems::A
.run_if(|res1: Res<RunConditionBool>| res1.is_changed())
.run_if(|res2: Res<Bool2>| res2.is_changed()),
);
schedule.add_systems(counting_system.in_set(TestSystems::A));
// both resource were just added.
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// nothing has changed
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// RunConditionBool has changed, but counting_system did not run
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// internal state for the bool2 run criteria was updated in the
// previous run, so system still does not run
world.get_resource_mut::<Bool2>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// internal state for bool2 was updated, so system still does not run
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// the system only runs when both are changed on the same run
world.get_resource_mut::<Bool2>().unwrap().0 = false;
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 2);
}
#[test]
fn mixed_conditions_and_change_detection() {
#[derive(Resource, Default)]
struct Bool2(pub bool);
let mut world = World::default();
world.init_resource::<Counter>();
world.init_resource::<RunConditionBool>();
world.init_resource::<Bool2>();
let mut schedule = Schedule::default();
schedule.configure_sets(
TestSystems::A.run_if(|res1: Res<RunConditionBool>| res1.is_changed()),
);
schedule.add_systems(
counting_system
.run_if(|res2: Res<Bool2>| res2.is_changed())
.in_set(TestSystems::A),
);
// both resource were just added.
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// nothing has changed
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// RunConditionBool has changed, but counting_system did not run
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// we now only change bool2 and the system also should not run
world.get_resource_mut::<Bool2>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// internal state for the bool2 run criteria was updated in the
// previous run, so system still does not run
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 1);
// the system only runs when both are changed on the same run
world.get_resource_mut::<Bool2>().unwrap().0 = false;
world.get_resource_mut::<RunConditionBool>().unwrap().0 = false;
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0.load(Ordering::Relaxed), 2);
}
}
mod schedule_build_errors {
use super::*;
#[test]
#[should_panic]
fn dependency_loop() {
let mut schedule = Schedule::default();
schedule.configure_sets(TestSystems::X.after(TestSystems::X));
}
#[test]
fn dependency_cycle() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.configure_sets(TestSystems::A.after(TestSystems::B));
schedule.configure_sets(TestSystems::B.after(TestSystems::A));
let result = schedule.initialize(&mut world);
assert!(matches!(
result,
Err(ScheduleBuildError::DependencyCycle(_))
));
fn foo() {}
fn bar() {}
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.add_systems((foo.after(bar), bar.after(foo)));
let result = schedule.initialize(&mut world);
assert!(matches!(
result,
Err(ScheduleBuildError::DependencyCycle(_))
));
}
#[test]
#[should_panic]
fn hierarchy_loop() {
let mut schedule = Schedule::default();
schedule.configure_sets(TestSystems::X.in_set(TestSystems::X));
}
#[test]
fn hierarchy_cycle() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.configure_sets(TestSystems::A.in_set(TestSystems::B));
schedule.configure_sets(TestSystems::B.in_set(TestSystems::A));
let result = schedule.initialize(&mut world);
assert!(matches!(result, Err(ScheduleBuildError::HierarchyCycle(_))));
}
#[test]
fn system_type_set_ambiguity() {
// Define some systems.
fn foo() {}
fn bar() {}
let mut world = World::new();
let mut schedule = Schedule::default();
// Schedule `bar` to run after `foo`.
schedule.add_systems((foo, bar.after(foo)));
// There's only one `foo`, so it's fine.
let result = schedule.initialize(&mut world);
assert!(result.is_ok());
// Schedule another `foo`.
schedule.add_systems(foo);
// When there are multiple instances of `foo`, dependencies on
// `foo` are no longer allowed. Too much ambiguity.
let result = schedule.initialize(&mut world);
assert!(matches!(
result,
Err(ScheduleBuildError::SystemTypeSetAmbiguity(_))
));
// same goes for `ambiguous_with`
let mut schedule = Schedule::default();
schedule.add_systems(foo);
schedule.add_systems(bar.ambiguous_with(foo));
let result = schedule.initialize(&mut world);
assert!(result.is_ok());
schedule.add_systems(foo);
let result = schedule.initialize(&mut world);
assert!(matches!(
result,
Err(ScheduleBuildError::SystemTypeSetAmbiguity(_))
));
}
#[test]
#[should_panic]
fn configure_system_type_set() {
fn foo() {}
let mut schedule = Schedule::default();
schedule.configure_sets(foo.into_system_set());
}
#[test]
fn hierarchy_redundancy() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.set_build_settings(ScheduleBuildSettings {
hierarchy_detection: LogLevel::Error,
..Default::default()
});
// Add `A`.
schedule.configure_sets(TestSystems::A);
// Add `B` as child of `A`.
schedule.configure_sets(TestSystems::B.in_set(TestSystems::A));
// Add `X` as child of both `A` and `B`.
schedule.configure_sets(TestSystems::X.in_set(TestSystems::A).in_set(TestSystems::B));
// `X` cannot be the `A`'s child and grandchild at the same time.
let result = schedule.initialize(&mut world);
assert!(matches!(
result,
Err(ScheduleBuildError::HierarchyRedundancy(_))
));
}
#[test]
fn cross_dependency() {
let mut world = World::new();
let mut schedule = Schedule::default();
// Add `B` and give it both kinds of relationships with `A`.
schedule.configure_sets(TestSystems::B.in_set(TestSystems::A));
schedule.configure_sets(TestSystems::B.after(TestSystems::A));
let result = schedule.initialize(&mut world);
assert!(matches!(
result,
Err(ScheduleBuildError::CrossDependency(_, _))
));
}
#[test]
fn sets_have_order_but_intersect() {
let mut world = World::new();
let mut schedule = Schedule::default();
fn foo() {}
// Add `foo` to both `A` and `C`.
schedule.add_systems(foo.in_set(TestSystems::A).in_set(TestSystems::C));
// Order `A -> B -> C`.
schedule.configure_sets((
TestSystems::A,
TestSystems::B.after(TestSystems::A),
TestSystems::C.after(TestSystems::B),
));
let result = schedule.initialize(&mut world);
// `foo` can't be in both `A` and `C` because they can't run at the same time.
assert!(matches!(
result,
Err(ScheduleBuildError::SetsHaveOrderButIntersect(_, _))
));
}
#[test]
fn ambiguity() {
#[derive(Resource)]
struct X;
fn res_ref(_x: Res<X>) {}
fn res_mut(_x: ResMut<X>) {}
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.set_build_settings(ScheduleBuildSettings {
ambiguity_detection: LogLevel::Error,
..Default::default()
});
schedule.add_systems((res_ref, res_mut));
let result = schedule.initialize(&mut world);
assert!(matches!(result, Err(ScheduleBuildError::Ambiguity(_))));
}
}
mod system_ambiguity {
use alloc::collections::BTreeSet;
use super::*;
use crate::prelude::*;
#[derive(Resource)]
struct R;
#[derive(Component)]
struct A;
#[derive(Component)]
struct B;
// An event type
#[derive(Event)]
struct E;
#[derive(Resource, Component)]
struct RC;
fn empty_system() {}
fn res_system(_res: Res<R>) {}
fn resmut_system(_res: ResMut<R>) {}
fn nonsend_system(_ns: NonSend<R>) {}
fn nonsendmut_system(_ns: NonSendMut<R>) {}
fn read_component_system(_query: Query<&A>) {}
fn write_component_system(_query: Query<&mut A>) {}
fn with_filtered_component_system(_query: Query<&mut A, With<B>>) {}
fn without_filtered_component_system(_query: Query<&mut A, Without<B>>) {}
fn entity_ref_system(_query: Query<EntityRef>) {}
fn entity_mut_system(_query: Query<EntityMut>) {}
fn event_reader_system(_reader: EventReader<E>) {}
fn event_writer_system(_writer: EventWriter<E>) {}
fn event_resource_system(_events: ResMut<Events<E>>) {}
fn read_world_system(_world: &World) {}
fn write_world_system(_world: &mut World) {}
// Tests for conflict detection
#[test]
fn one_of_everything() {
let mut world = World::new();
world.insert_resource(R);
world.spawn(A);
world.init_resource::<Events<E>>();
let mut schedule = Schedule::default();
schedule
// nonsendmut system deliberately conflicts with resmut system
.add_systems((resmut_system, write_component_system, event_writer_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn read_only() {
let mut world = World::new();
world.insert_resource(R);
world.spawn(A);
world.init_resource::<Events<E>>();
let mut schedule = Schedule::default();
schedule.add_systems((
empty_system,
empty_system,
res_system,
res_system,
nonsend_system,
nonsend_system,
read_component_system,
read_component_system,
entity_ref_system,
entity_ref_system,
event_reader_system,
event_reader_system,
read_world_system,
read_world_system,
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn read_world() {
let mut world = World::new();
world.insert_resource(R);
world.spawn(A);
world.init_resource::<Events<E>>();
let mut schedule = Schedule::default();
schedule.add_systems((
resmut_system,
write_component_system,
event_writer_system,
read_world_system,
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 3);
}
#[test]
fn resources() {
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::default();
schedule.add_systems((resmut_system, res_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 1);
}
#[test]
fn nonsend() {
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::default();
schedule.add_systems((nonsendmut_system, nonsend_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 1);
}
#[test]
fn components() {
let mut world = World::new();
world.spawn(A);
let mut schedule = Schedule::default();
schedule.add_systems((read_component_system, write_component_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 1);
}
#[test]
#[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"]
fn filtered_components() {
let mut world = World::new();
world.spawn(A);
let mut schedule = Schedule::default();
schedule.add_systems((
with_filtered_component_system,
without_filtered_component_system,
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn events() {
let mut world = World::new();
world.init_resource::<Events<E>>();
let mut schedule = Schedule::default();
schedule.add_systems((
// All of these systems clash
event_reader_system,
event_writer_system,
event_resource_system,
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 3);
}
/// Test that when a struct is both a Resource and a Component, they do not
/// conflict with each other.
#[test]
fn shared_resource_mut_component() {
let mut world = World::new();
world.insert_resource(RC);
let mut schedule = Schedule::default();
schedule.add_systems((|_: ResMut<RC>| {}, |_: Query<&mut RC>| {}));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn resource_mut_and_entity_ref() {
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::default();
schedule.add_systems((resmut_system, entity_ref_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn resource_and_entity_mut() {
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::default();
schedule.add_systems((res_system, nonsend_system, entity_mut_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn write_component_and_entity_ref() {
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::default();
schedule.add_systems((write_component_system, entity_ref_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 1);
}
#[test]
fn read_component_and_entity_mut() {
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::default();
schedule.add_systems((read_component_system, entity_mut_system));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 1);
}
#[test]
fn exclusive() {
let mut world = World::new();
world.insert_resource(R);
world.spawn(A);
world.init_resource::<Events<E>>();
let mut schedule = Schedule::default();
schedule.add_systems((
// All 3 of these conflict with each other
write_world_system,
write_world_system,
res_system,
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 3);
}
// Tests for silencing and resolving ambiguities
#[test]
fn before_and_after() {
let mut world = World::new();
world.init_resource::<Events<E>>();
let mut schedule = Schedule::default();
schedule.add_systems((
event_reader_system.before(event_writer_system),
event_writer_system,
event_resource_system.after(event_writer_system),
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn ignore_all_ambiguities() {
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::default();
schedule.add_systems((
resmut_system.ambiguous_with_all(),
res_system,
nonsend_system,
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn ambiguous_with_label() {
let mut world = World::new();
world.insert_resource(R);
#[derive(SystemSet, Hash, PartialEq, Eq, Debug, Clone)]
struct IgnoreMe;
let mut schedule = Schedule::default();
schedule.add_systems((
resmut_system.ambiguous_with(IgnoreMe),
res_system.in_set(IgnoreMe),
nonsend_system.in_set(IgnoreMe),
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[test]
fn ambiguous_with_system() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.add_systems((
write_component_system.ambiguous_with(read_component_system),
read_component_system,
));
let _ = schedule.initialize(&mut world);
assert_eq!(schedule.graph().conflicting_systems().len(), 0);
}
#[derive(ScheduleLabel, Hash, PartialEq, Eq, Debug, Clone)]
struct TestSchedule;
// Tests that the correct ambiguities were reported in the correct order.
#[test]
fn correct_ambiguities() {
fn system_a(_res: ResMut<R>) {}
fn system_b(_res: ResMut<R>) {}
fn system_c(_res: ResMut<R>) {}
fn system_d(_res: ResMut<R>) {}
fn system_e(_res: ResMut<R>) {}
let mut world = World::new();
world.insert_resource(R);
let mut schedule = Schedule::new(TestSchedule);
schedule.add_systems((
system_a,
system_b,
system_c.ambiguous_with_all(),
system_d.ambiguous_with(system_b),
system_e.after(system_a),
));
schedule.graph_mut().initialize(&mut world);
let _ = schedule.graph_mut().build_schedule(
&mut world,
TestSchedule.intern(),
&BTreeSet::new(),
);
let ambiguities: Vec<_> = schedule
.graph()
.conflicts_to_string(schedule.graph().conflicting_systems(), world.components())
.map(|item| {
(
item.0,
item.1,
item.2
.into_iter()
.map(|name| name.to_string())
.collect::<Vec<_>>(),
)
})
.collect();
let expected = &[
(
"system_d".to_string(),
"system_a".to_string(),
vec!["bevy_ecs::schedule::tests::system_ambiguity::R".into()],
),
(
"system_d".to_string(),
"system_e".to_string(),
vec!["bevy_ecs::schedule::tests::system_ambiguity::R".into()],
),
(
"system_b".to_string(),
"system_a".to_string(),
vec!["bevy_ecs::schedule::tests::system_ambiguity::R".into()],
),
(
"system_b".to_string(),
"system_e".to_string(),
vec!["bevy_ecs::schedule::tests::system_ambiguity::R".into()],
),
];
// ordering isn't stable so do this
for entry in expected {
assert!(ambiguities.contains(entry));
}
}
// Test that anonymous set names work properly
// Related issue https://github.com/bevyengine/bevy/issues/9641
#[test]
fn anonymous_set_name() {
let mut schedule = Schedule::new(TestSchedule);
schedule.add_systems((resmut_system, resmut_system).run_if(|| true));
let mut world = World::new();
schedule.graph_mut().initialize(&mut world);
let _ = schedule.graph_mut().build_schedule(
&mut world,
TestSchedule.intern(),
&BTreeSet::new(),
);
let ambiguities: Vec<_> = schedule
.graph()
.conflicts_to_string(schedule.graph().conflicting_systems(), world.components())
.map(|item| {
(
item.0,
item.1,
item.2
.into_iter()
.map(|name| name.to_string())
.collect::<Vec<_>>(),
)
})
.collect();
assert_eq!(
ambiguities[0],
(
"resmut_system (in set (resmut_system, resmut_system))".to_string(),
"resmut_system (in set (resmut_system, resmut_system))".to_string(),
vec!["bevy_ecs::schedule::tests::system_ambiguity::R".into()],
)
);
}
#[test]
fn ignore_component_resource_ambiguities() {
let mut world = World::new();
world.insert_resource(R);
world.allow_ambiguous_resource::<R>();
let mut schedule = Schedule::new(TestSchedule);
// check resource
schedule.add_systems((resmut_system, res_system));
schedule.initialize(&mut world).unwrap();
assert!(schedule.graph().conflicting_systems().is_empty());
// check components
world.allow_ambiguous_component::<A>();
schedule.add_systems((write_component_system, read_component_system));
schedule.initialize(&mut world).unwrap();
assert!(schedule.graph().conflicting_systems().is_empty());
}
}
#[cfg(feature = "bevy_debug_stepping")]
mod stepping {
use super::*;
use bevy_ecs::system::SystemState;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TestSchedule;
macro_rules! assert_executor_supports_stepping {
($executor:expr) => {
// create a test schedule
let mut schedule = Schedule::new(TestSchedule);
schedule
.set_executor_kind($executor)
.add_systems(|| -> () { panic!("Executor ignored Stepping") });
// Add our schedule to stepping & and enable stepping; this should
// prevent any systems in the schedule from running
let mut stepping = Stepping::default();
stepping.add_schedule(TestSchedule).enable();
// create a world, and add the stepping resource
let mut world = World::default();
world.insert_resource(stepping);
// start a new frame by running ihe begin_frame() system
let mut system_state: SystemState<Option<ResMut<Stepping>>> =
SystemState::new(&mut world);
let res = system_state.get_mut(&mut world);
Stepping::begin_frame(res);
// now run the schedule; this will panic if the executor doesn't
// handle stepping
schedule.run(&mut world);
};
}
/// verify the [`SimpleExecutor`] supports stepping
#[test]
#[expect(deprecated, reason = "We still need to test this.")]
fn simple_executor() {
assert_executor_supports_stepping!(ExecutorKind::Simple);
}
/// verify the [`SingleThreadedExecutor`] supports stepping
#[test]
fn single_threaded_executor() {
assert_executor_supports_stepping!(ExecutorKind::SingleThreaded);
}
/// verify the [`MultiThreadedExecutor`] supports stepping
#[test]
fn multi_threaded_executor() {
assert_executor_supports_stepping!(ExecutorKind::MultiThreaded);
}
}
}