bevy/crates/bevy_ecs/src/schedule/mod.rs
Guillaume Wafo-Tapa 0e805eb49c
Implement SystemCondition for systems returning Result<bool, BevyError> and Result<(), BevyError> (#19553)
# Objective

Fixes #19403
As described in the issue, the objective is to support the use of
systems returning `Result<(), BevyError>` and
`Result<bool, BevyError>` as run conditions. In these cases, the run
condition would hold on `Ok(())` and `Ok(true)` respectively.


## Solution

`IntoSystem<In, bool, M>` cannot be implemented for systems returning
`Result<(), BevyError>` and `Result<bool, BevyError>` as that would
conflict with their trivial implementation of the trait. That led me to
add a method to the sealed trait `SystemCondition` that does the
conversion. In the original case of a system returning `bool`, the
system is returned as is. With the new types, the system is combined
with `map()` to obtain a `bool`.

By the way, I'm confused as to why `SystemCondition` has a generic `In`
parameter as it is only ever used with `In = ()` as far as I can tell.

## Testing

I added a simple test for both type of system. That's minimal but it
felt enough. I could not picture the more complicated tests passing for
a run condition returning `bool` and failing for the new types.

## Doc

I documenting the change on the page of the trait. I had trouble wording
it right but I'm not sure how to improve it. The phrasing "the condition
returns `true`" which reads naturally is now technically incorrect as
the new types return a `Result`. However, the underlying condition
system that the implementing system turns into does indeed return
`bool`. But talking about the implementation details felt too much.
Another possibility is to use another turn of phrase like "the condition
holds" or "the condition checks out". I've left "the condition returns
`true`" in the documentation of `run_if` and the provided methods for
now.

I'm perplexed about the examples. In the first one, why not implement
the condition directly instead of having a system returning it? Is it
from a time of Bevy where you had to implement your conditions that way?
In that case maybe that should be updated. And in the second example I'm
missing the point entirely. As I stated above, I've only seen conditions
used in contexts where they have no input parameter. Here we create a
condition with an input parameter (cannot be used by `run_if`) and we
are using it with `pipe()` which actually doesn't need our system to
implement `SystemCondition`. Both examples are also calling
`IntoSystem::into_system` which should not be encouraged. What am I
missing?
2025-06-10 00:04:10 +00:00

1301 lines
44 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};
use crate::error::BevyError;
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(bool);
#[derive(Resource, Default)]
struct Counter(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_bool() {
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 system_with_condition_result_unit() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems(
make_function_system(0).run_if(|| Err::<(), BevyError>(core::fmt::Error.into())),
);
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);
schedule.add_systems(make_function_system(1).run_if(|| Ok(())));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![1]);
}
#[test]
fn system_with_condition_result_bool() {
let mut world = World::default();
let mut schedule = Schedule::default();
world.init_resource::<SystemOrder>();
schedule.add_systems((
make_function_system(0).run_if(|| Err::<bool, BevyError>(core::fmt::Error.into())),
make_function_system(1).run_if(|| Ok(false)),
));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![]);
schedule.add_systems(make_function_system(2).run_if(|| Ok(true)));
schedule.run(&mut world);
assert_eq!(world.resource::<SystemOrder>().0, vec![2]);
}
#[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]
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);
}
}
}