Skip to content

Commit 31bb878

Browse files
Fix system param validation for piped systems (#18785)
# Objective - Piped systems are an edge case that we missed when reworking system parameter validation. - Fixes #18755. ## Solution - Validate the parameters for both systems, ~~combining the errors if both failed validation~~ by simply using an early out. - ~~Also fix the same bug for combinator systems while we're here.~~ ## Testing I've added a large number of tests checking the behavior under various permutations. These are separate tests, rather than one mega test because a) it's easier to track down bugs that way and b) many of these are `should_panic` tests, which will halt the evaluation of the rest of the test! I've also added a test for exclusive systems being pipeable because we don't have one and I was very surprised that that works! --------- Co-authored-by: Chris Russell <[email protected]>
1 parent 2944f5e commit 31bb878

File tree

2 files changed

+201
-7
lines changed

2 files changed

+201
-7
lines changed

crates/bevy_ecs/src/schedule/executor/mod.rs

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ mod __rust_begin_short_backtrace {
315315
#[cfg(test)]
316316
mod tests {
317317
use crate::{
318-
prelude::{Component, Resource, Schedule},
318+
prelude::{Component, In, IntoSystem, Resource, Schedule},
319319
schedule::ExecutorKind,
320320
system::{Populated, Res, ResMut, Single},
321321
world::World,
@@ -336,6 +336,9 @@ mod tests {
336336
single_ran: bool,
337337
}
338338

339+
#[derive(Resource, Default)]
340+
struct Counter(u8);
341+
339342
fn set_single_state(mut _single: Single<&TestComponent>, mut state: ResMut<TestState>) {
340343
state.single_ran = true;
341344
}
@@ -408,4 +411,162 @@ mod tests {
408411
schedule.add_systems(look_for_missing_resource);
409412
schedule.run(&mut world);
410413
}
414+
415+
#[test]
416+
fn piped_systems_first_system_skipped() {
417+
// This system should be skipped when run due to no matching entity
418+
fn pipe_out(_single: Single<&TestComponent>) -> u8 {
419+
42
420+
}
421+
422+
fn pipe_in(_input: In<u8>, mut counter: ResMut<Counter>) {
423+
counter.0 += 1;
424+
}
425+
426+
let mut world = World::new();
427+
world.init_resource::<Counter>();
428+
let mut schedule = Schedule::default();
429+
430+
schedule.add_systems(pipe_out.pipe(pipe_in));
431+
schedule.run(&mut world);
432+
433+
let counter = world.resource::<Counter>();
434+
assert_eq!(counter.0, 0);
435+
}
436+
437+
#[test]
438+
fn piped_system_second_system_skipped() {
439+
fn pipe_out(mut counter: ResMut<Counter>) -> u8 {
440+
counter.0 += 1;
441+
42
442+
}
443+
444+
// This system should be skipped when run due to no matching entity
445+
fn pipe_in(_input: In<u8>, _single: Single<&TestComponent>) {}
446+
447+
let mut world = World::new();
448+
world.init_resource::<Counter>();
449+
let mut schedule = Schedule::default();
450+
451+
schedule.add_systems(pipe_out.pipe(pipe_in));
452+
schedule.run(&mut world);
453+
let counter = world.resource::<Counter>();
454+
assert_eq!(counter.0, 0);
455+
}
456+
457+
#[test]
458+
#[should_panic]
459+
fn piped_system_first_system_panics() {
460+
// This system should panic when run because the resource is missing
461+
fn pipe_out(_res: Res<TestState>) -> u8 {
462+
42
463+
}
464+
465+
fn pipe_in(_input: In<u8>) {}
466+
467+
let mut world = World::new();
468+
let mut schedule = Schedule::default();
469+
470+
schedule.add_systems(pipe_out.pipe(pipe_in));
471+
schedule.run(&mut world);
472+
}
473+
474+
#[test]
475+
#[should_panic]
476+
fn piped_system_second_system_panics() {
477+
fn pipe_out() -> u8 {
478+
42
479+
}
480+
481+
// This system should panic when run because the resource is missing
482+
fn pipe_in(_input: In<u8>, _res: Res<TestState>) {}
483+
484+
let mut world = World::new();
485+
let mut schedule = Schedule::default();
486+
487+
schedule.add_systems(pipe_out.pipe(pipe_in));
488+
schedule.run(&mut world);
489+
}
490+
491+
// This test runs without panicking because we've
492+
// decided to use early-out behavior for piped systems
493+
#[test]
494+
fn piped_system_skip_and_panic() {
495+
// This system should be skipped when run due to no matching entity
496+
fn pipe_out(_single: Single<&TestComponent>) -> u8 {
497+
42
498+
}
499+
500+
// This system should panic when run because the resource is missing
501+
fn pipe_in(_input: In<u8>, _res: Res<TestState>) {}
502+
503+
let mut world = World::new();
504+
let mut schedule = Schedule::default();
505+
506+
schedule.add_systems(pipe_out.pipe(pipe_in));
507+
schedule.run(&mut world);
508+
}
509+
510+
#[test]
511+
#[should_panic]
512+
fn piped_system_panic_and_skip() {
513+
// This system should panic when run because the resource is missing
514+
515+
fn pipe_out(_res: Res<TestState>) -> u8 {
516+
42
517+
}
518+
519+
// This system should be skipped when run due to no matching entity
520+
fn pipe_in(_input: In<u8>, _single: Single<&TestComponent>) {}
521+
522+
let mut world = World::new();
523+
let mut schedule = Schedule::default();
524+
525+
schedule.add_systems(pipe_out.pipe(pipe_in));
526+
schedule.run(&mut world);
527+
}
528+
529+
#[test]
530+
#[should_panic]
531+
fn piped_system_panic_and_panic() {
532+
// This system should panic when run because the resource is missing
533+
534+
fn pipe_out(_res: Res<TestState>) -> u8 {
535+
42
536+
}
537+
538+
// This system should panic when run because the resource is missing
539+
fn pipe_in(_input: In<u8>, _res: Res<TestState>) {}
540+
541+
let mut world = World::new();
542+
let mut schedule = Schedule::default();
543+
544+
schedule.add_systems(pipe_out.pipe(pipe_in));
545+
schedule.run(&mut world);
546+
}
547+
548+
#[test]
549+
fn piped_system_skip_and_skip() {
550+
// This system should be skipped when run due to no matching entity
551+
552+
fn pipe_out(_single: Single<&TestComponent>, mut counter: ResMut<Counter>) -> u8 {
553+
counter.0 += 1;
554+
42
555+
}
556+
557+
// This system should be skipped when run due to no matching entity
558+
fn pipe_in(_input: In<u8>, _single: Single<&TestComponent>, mut counter: ResMut<Counter>) {
559+
counter.0 += 1;
560+
}
561+
562+
let mut world = World::new();
563+
world.init_resource::<Counter>();
564+
let mut schedule = Schedule::default();
565+
566+
schedule.add_systems(pipe_out.pipe(pipe_in));
567+
schedule.run(&mut world);
568+
569+
let counter = world.resource::<Counter>();
570+
assert_eq!(counter.0, 0);
571+
}
411572
}

crates/bevy_ecs/src/system/combinator.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -417,17 +417,26 @@ where
417417
self.b.queue_deferred(world);
418418
}
419419

420+
/// This method uses "early out" logic: if the first system fails validation,
421+
/// the second system is not validated.
422+
///
423+
/// Because the system validation is performed upfront, this can lead to situations
424+
/// where later systems pass validation, but fail at runtime due to changes made earlier
425+
/// in the piped systems.
426+
// TODO: ensure that systems are only validated just before they are run.
427+
// Fixing this will require fundamentally rethinking how piped systems work:
428+
// they're currently treated as a single system from the perspective of the scheduler.
429+
// See https://github.com/bevyengine/bevy/issues/18796
420430
unsafe fn validate_param_unsafe(
421431
&mut self,
422432
world: UnsafeWorldCell,
423433
) -> Result<(), SystemParamValidationError> {
424-
// SAFETY: Delegate to other `System` implementations.
425-
unsafe { self.a.validate_param_unsafe(world) }
426-
}
434+
// SAFETY: Delegate to the `System` implementation for `a`.
435+
unsafe { self.a.validate_param_unsafe(world) }?;
436+
437+
// SAFETY: Delegate to the `System` implementation for `b`.
438+
unsafe { self.b.validate_param_unsafe(world) }?;
427439

428-
fn validate_param(&mut self, world: &World) -> Result<(), SystemParamValidationError> {
429-
self.a.validate_param(world)?;
430-
self.b.validate_param(world)?;
431440
Ok(())
432441
}
433442

@@ -477,3 +486,27 @@ where
477486
for<'a> B::In: SystemInput<Inner<'a> = A::Out>,
478487
{
479488
}
489+
490+
#[cfg(test)]
491+
mod tests {
492+
493+
#[test]
494+
fn exclusive_system_piping_is_possible() {
495+
use crate::prelude::*;
496+
497+
fn my_exclusive_system(_world: &mut World) -> u32 {
498+
1
499+
}
500+
501+
fn out_pipe(input: In<u32>) {
502+
assert!(input.0 == 1);
503+
}
504+
505+
let mut world = World::new();
506+
507+
let mut schedule = Schedule::default();
508+
schedule.add_systems(my_exclusive_system.pipe(out_pipe));
509+
510+
schedule.run(&mut world);
511+
}
512+
}

0 commit comments

Comments
 (0)