mirror of
https://github.com/bevyengine/bevy
synced 2024-11-14 08:58:04 +00:00
Add ambiguity detection tests (#6053)
# Objective - Add unit tests for ambiguity detection reporting. - Incremental implementation of #4299. ## Solution - Refactor ambiguity detection internals to make it testable. As a bonus, this should make it easier to extend in the future. ## Notes * This code was copy-pasted from #4299 and modified. Credit goes to @alice-i-cecile and @afonsolage, though I'm not sure who wrote what at this point.
This commit is contained in:
parent
e668b47277
commit
fb74ca3d46
1 changed files with 417 additions and 58 deletions
|
@ -5,75 +5,216 @@ use crate::component::ComponentId;
|
||||||
use crate::schedule::{SystemContainer, SystemStage};
|
use crate::schedule::{SystemContainer, SystemStage};
|
||||||
use crate::world::World;
|
use crate::world::World;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
struct SystemOrderAmbiguity {
|
||||||
|
segment: SystemStageSegment,
|
||||||
|
// Note: In order for comparisons to work correctly,
|
||||||
|
// `system_names` and `conflicts` must be sorted at all times.
|
||||||
|
system_names: [String; 2],
|
||||||
|
conflicts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which part of a [`SystemStage`] was a [`SystemOrderAmbiguity`] detected in?
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)]
|
||||||
|
enum SystemStageSegment {
|
||||||
|
Parallel,
|
||||||
|
ExclusiveAtStart,
|
||||||
|
ExclusiveBeforeCommands,
|
||||||
|
ExclusiveAtEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemStageSegment {
|
||||||
|
pub fn desc(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SystemStageSegment::Parallel => "Parallel systems",
|
||||||
|
SystemStageSegment::ExclusiveAtStart => "Exclusive systems at start of stage",
|
||||||
|
SystemStageSegment::ExclusiveBeforeCommands => {
|
||||||
|
"Exclusive systems before commands of stage"
|
||||||
|
}
|
||||||
|
SystemStageSegment::ExclusiveAtEnd => "Exclusive systems at end of stage",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemOrderAmbiguity {
|
||||||
|
fn from_raw(
|
||||||
|
system_a_index: usize,
|
||||||
|
system_b_index: usize,
|
||||||
|
component_ids: Vec<ComponentId>,
|
||||||
|
segment: SystemStageSegment,
|
||||||
|
stage: &SystemStage,
|
||||||
|
world: &World,
|
||||||
|
) -> Self {
|
||||||
|
use crate::schedule::graph_utils::GraphNode;
|
||||||
|
use SystemStageSegment::*;
|
||||||
|
|
||||||
|
// TODO: blocked on https://github.com/bevyengine/bevy/pull/4166
|
||||||
|
// We can't grab the system container generically, because .parallel_systems()
|
||||||
|
// and the exclusive equivalent return a different type,
|
||||||
|
// and SystemContainer is not object-safe
|
||||||
|
let (system_a_name, system_b_name) = match segment {
|
||||||
|
Parallel => {
|
||||||
|
let system_container = stage.parallel_systems();
|
||||||
|
(
|
||||||
|
system_container[system_a_index].name(),
|
||||||
|
system_container[system_b_index].name(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExclusiveAtStart => {
|
||||||
|
let system_container = stage.exclusive_at_start_systems();
|
||||||
|
(
|
||||||
|
system_container[system_a_index].name(),
|
||||||
|
system_container[system_b_index].name(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExclusiveBeforeCommands => {
|
||||||
|
let system_container = stage.exclusive_before_commands_systems();
|
||||||
|
(
|
||||||
|
system_container[system_a_index].name(),
|
||||||
|
system_container[system_b_index].name(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExclusiveAtEnd => {
|
||||||
|
let system_container = stage.exclusive_at_end_systems();
|
||||||
|
(
|
||||||
|
system_container[system_a_index].name(),
|
||||||
|
system_container[system_b_index].name(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut system_names = [system_a_name.to_string(), system_b_name.to_string()];
|
||||||
|
system_names.sort();
|
||||||
|
|
||||||
|
let mut conflicts: Vec<_> = component_ids
|
||||||
|
.iter()
|
||||||
|
.map(|id| world.components().get_info(*id).unwrap().name().to_owned())
|
||||||
|
.collect();
|
||||||
|
conflicts.sort();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
system_names,
|
||||||
|
conflicts,
|
||||||
|
segment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SystemStage {
|
impl SystemStage {
|
||||||
/// Logs execution order ambiguities between systems. System orders must be fresh.
|
/// Logs execution order ambiguities between systems.
|
||||||
|
///
|
||||||
|
/// The output may be incorrect if this stage has not been initialized with `world`.
|
||||||
pub fn report_ambiguities(&self, world: &World) {
|
pub fn report_ambiguities(&self, world: &World) {
|
||||||
debug_assert!(!self.systems_modified);
|
debug_assert!(!self.systems_modified);
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
fn write_display_names_of_pairs(
|
let ambiguities = self.ambiguities(world);
|
||||||
string: &mut String,
|
if !ambiguities.is_empty() {
|
||||||
systems: &[impl SystemContainer],
|
|
||||||
mut ambiguities: Vec<(usize, usize, Vec<ComponentId>)>,
|
|
||||||
world: &World,
|
|
||||||
) {
|
|
||||||
for (index_a, index_b, conflicts) in ambiguities.drain(..) {
|
|
||||||
writeln!(
|
|
||||||
string,
|
|
||||||
" -- {:?} and {:?}",
|
|
||||||
systems[index_a].name(),
|
|
||||||
systems[index_b].name()
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
if !conflicts.is_empty() {
|
|
||||||
let names = conflicts
|
|
||||||
.iter()
|
|
||||||
.map(|id| world.components().get_info(*id).unwrap().name())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
writeln!(string, " conflicts: {:?}", names).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let parallel = find_ambiguities(&self.parallel);
|
|
||||||
let at_start = find_ambiguities(&self.exclusive_at_start);
|
|
||||||
let before_commands = find_ambiguities(&self.exclusive_before_commands);
|
|
||||||
let at_end = find_ambiguities(&self.exclusive_at_end);
|
|
||||||
if !(parallel.is_empty()
|
|
||||||
&& at_start.is_empty()
|
|
||||||
&& before_commands.is_empty()
|
|
||||||
&& at_end.is_empty())
|
|
||||||
{
|
|
||||||
let mut string = "Execution order ambiguities detected, you might want to \
|
let mut string = "Execution order ambiguities detected, you might want to \
|
||||||
add an explicit dependency relation between some of these systems:\n"
|
add an explicit dependency relation between some of these systems:\n"
|
||||||
.to_owned();
|
.to_owned();
|
||||||
if !parallel.is_empty() {
|
|
||||||
writeln!(string, " * Parallel systems:").unwrap();
|
let mut last_segment_kind = None;
|
||||||
write_display_names_of_pairs(&mut string, &self.parallel, parallel, world);
|
for SystemOrderAmbiguity {
|
||||||
|
system_names: [system_a, system_b],
|
||||||
|
conflicts,
|
||||||
|
segment,
|
||||||
|
} in &ambiguities
|
||||||
|
{
|
||||||
|
// If the ambiguity occurred in a different segment than the previous one, write a header for the segment.
|
||||||
|
if last_segment_kind != Some(segment) {
|
||||||
|
writeln!(string, " * {}:", segment.desc()).unwrap();
|
||||||
|
last_segment_kind = Some(segment);
|
||||||
}
|
}
|
||||||
if !at_start.is_empty() {
|
|
||||||
writeln!(string, " * Exclusive systems at start of stage:").unwrap();
|
writeln!(string, " -- {:?} and {:?}", system_a, system_b).unwrap();
|
||||||
write_display_names_of_pairs(
|
|
||||||
&mut string,
|
if !conflicts.is_empty() {
|
||||||
&self.exclusive_at_start,
|
writeln!(string, " conflicts: {conflicts:?}").unwrap();
|
||||||
at_start,
|
|
||||||
world,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if !before_commands.is_empty() {
|
|
||||||
writeln!(string, " * Exclusive systems before commands of stage:").unwrap();
|
|
||||||
write_display_names_of_pairs(
|
|
||||||
&mut string,
|
|
||||||
&self.exclusive_before_commands,
|
|
||||||
before_commands,
|
|
||||||
world,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if !at_end.is_empty() {
|
|
||||||
writeln!(string, " * Exclusive systems at end of stage:").unwrap();
|
|
||||||
write_display_names_of_pairs(&mut string, &self.exclusive_at_end, at_end, world);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("{}", string);
|
info!("{}", string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns all execution order ambiguities between systems.
|
||||||
|
///
|
||||||
|
/// Returns 4 vectors of ambiguities for each stage, in the following order:
|
||||||
|
/// - parallel
|
||||||
|
/// - exclusive at start,
|
||||||
|
/// - exclusive before commands
|
||||||
|
/// - exclusive at end
|
||||||
|
///
|
||||||
|
/// The result may be incorrect if this stage has not been initialized with `world`.
|
||||||
|
fn ambiguities(&self, world: &World) -> Vec<SystemOrderAmbiguity> {
|
||||||
|
let parallel = find_ambiguities(&self.parallel).into_iter().map(
|
||||||
|
|(system_a_index, system_b_index, component_ids)| {
|
||||||
|
SystemOrderAmbiguity::from_raw(
|
||||||
|
system_a_index,
|
||||||
|
system_b_index,
|
||||||
|
component_ids.to_vec(),
|
||||||
|
SystemStageSegment::Parallel,
|
||||||
|
self,
|
||||||
|
world,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let at_start = find_ambiguities(&self.exclusive_at_start).into_iter().map(
|
||||||
|
|(system_a_index, system_b_index, component_ids)| {
|
||||||
|
SystemOrderAmbiguity::from_raw(
|
||||||
|
system_a_index,
|
||||||
|
system_b_index,
|
||||||
|
component_ids,
|
||||||
|
SystemStageSegment::ExclusiveAtStart,
|
||||||
|
self,
|
||||||
|
world,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let before_commands = find_ambiguities(&self.exclusive_before_commands)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(system_a_index, system_b_index, component_ids)| {
|
||||||
|
SystemOrderAmbiguity::from_raw(
|
||||||
|
system_a_index,
|
||||||
|
system_b_index,
|
||||||
|
component_ids,
|
||||||
|
SystemStageSegment::ExclusiveBeforeCommands,
|
||||||
|
self,
|
||||||
|
world,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let at_end = find_ambiguities(&self.exclusive_at_end).into_iter().map(
|
||||||
|
|(system_a_index, system_b_index, component_ids)| {
|
||||||
|
SystemOrderAmbiguity::from_raw(
|
||||||
|
system_a_index,
|
||||||
|
system_b_index,
|
||||||
|
component_ids,
|
||||||
|
SystemStageSegment::ExclusiveAtEnd,
|
||||||
|
self,
|
||||||
|
world,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut ambiguities: Vec<_> = at_start
|
||||||
|
.chain(parallel)
|
||||||
|
.chain(before_commands)
|
||||||
|
.chain(at_end)
|
||||||
|
.collect();
|
||||||
|
ambiguities.sort();
|
||||||
|
ambiguities
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of system order ambiguities between systems in this stage.
|
||||||
|
///
|
||||||
|
/// The result may be incorrect if this stage has not been initialized with `world`.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn ambiguity_count(&self, world: &World) -> usize {
|
||||||
|
self.ambiguities(world).len()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns vector containing all pairs of indices of systems with ambiguous execution order,
|
/// Returns vector containing all pairs of indices of systems with ambiguous execution order,
|
||||||
|
@ -138,3 +279,221 @@ fn find_ambiguities(systems: &[impl SystemContainer]) -> Vec<(usize, usize, Vec<
|
||||||
}
|
}
|
||||||
ambiguities
|
ambiguities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
// Required to make the derive macro behave
|
||||||
|
use crate as bevy_ecs;
|
||||||
|
use crate::event::Events;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct R;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct A;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct B;
|
||||||
|
|
||||||
|
// An event type
|
||||||
|
struct E;
|
||||||
|
|
||||||
|
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 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().insert(A);
|
||||||
|
world.init_resource::<Events<E>>();
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
// nonsendmut system deliberately conflicts with resmut system
|
||||||
|
.add_system(resmut_system)
|
||||||
|
.add_system(write_component_system)
|
||||||
|
.add_system(event_writer_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_only() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.insert_resource(R);
|
||||||
|
world.spawn().insert(A);
|
||||||
|
world.init_resource::<Events<E>>();
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
.add_system(empty_system)
|
||||||
|
.add_system(empty_system)
|
||||||
|
.add_system(res_system)
|
||||||
|
.add_system(res_system)
|
||||||
|
.add_system(nonsend_system)
|
||||||
|
.add_system(nonsend_system)
|
||||||
|
.add_system(read_component_system)
|
||||||
|
.add_system(read_component_system)
|
||||||
|
.add_system(event_reader_system)
|
||||||
|
.add_system(event_reader_system)
|
||||||
|
.add_system(read_world_system)
|
||||||
|
.add_system(read_world_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_world() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.insert_resource(R);
|
||||||
|
world.spawn().insert(A);
|
||||||
|
world.init_resource::<Events<E>>();
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
.add_system(resmut_system)
|
||||||
|
.add_system(write_component_system)
|
||||||
|
.add_system(event_writer_system)
|
||||||
|
.add_system(read_world_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resources() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.insert_resource(R);
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage.add_system(resmut_system).add_system(res_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonsend() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.insert_resource(R);
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
.add_system(nonsendmut_system)
|
||||||
|
.add_system(nonsend_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn components() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.spawn().insert(A);
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
.add_system(read_component_system)
|
||||||
|
.add_system(write_component_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 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().insert(A);
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
.add_system(with_filtered_component_system)
|
||||||
|
.add_system(without_filtered_component_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn events() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<Events<E>>();
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
// All of these systems clash
|
||||||
|
.add_system(event_reader_system)
|
||||||
|
.add_system(event_writer_system)
|
||||||
|
.add_system(event_resource_system);
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclusive() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.insert_resource(R);
|
||||||
|
world.spawn().insert(A);
|
||||||
|
world.init_resource::<Events<E>>();
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
// All 3 of these conflict with each other
|
||||||
|
.add_system(write_world_system.exclusive_system())
|
||||||
|
.add_system(write_world_system.exclusive_system().at_end())
|
||||||
|
.add_system(res_system.exclusive_system())
|
||||||
|
// These do not, as they're in different segments of the stage
|
||||||
|
.add_system(write_world_system.exclusive_system().at_start())
|
||||||
|
.add_system(write_world_system.exclusive_system().before_commands());
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for silencing and resolving ambiguities
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn before_and_after() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<Events<E>>();
|
||||||
|
|
||||||
|
let mut test_stage = SystemStage::parallel();
|
||||||
|
test_stage
|
||||||
|
.add_system(event_reader_system.before(event_writer_system))
|
||||||
|
.add_system(event_writer_system)
|
||||||
|
.add_system(event_resource_system.after(event_writer_system));
|
||||||
|
|
||||||
|
test_stage.run(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(test_stage.ambiguity_count(&world), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue