Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 298 additions & 7 deletions crates/bevy_ecs/src/schedule/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,15 @@ where
a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<A::Out, RunSystemError>,
b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<B::Out, RunSystemError>,
) -> Result<Self::Out, RunSystemError> {
Ok(!(a(input, data)? && b(input, data)?))
match a(input, data) {
// short circuit because `a` && `b` short circuits if `a` is false
Ok(false) | Err(_) => Ok(true),
Ok(true) => match b(input, data) {
Ok(b) => Ok(!b),
// if `b` fails, we yield `true`
Err(_) => Ok(true),
},
}
}
}

Expand All @@ -1238,7 +1246,18 @@ where
a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<A::Out, RunSystemError>,
b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<B::Out, RunSystemError>,
) -> Result<Self::Out, RunSystemError> {
Ok(!(a(input, data)? || b(input, data)?))
// `a` might fail to validate against the current world, but if it
// fails, we still want to try running `b`.
// !(`a` || `b`)
match a(input, data) {
Ok(true) => Ok(false),
a @ (Ok(false) | Err(_)) => match (a, b(input, data)) {
(_, Ok(false) | Err(_)) => Ok(true),
// propagate error
(Err(e), _) => Err(e),
(_, Ok(true)) => Ok(false),
},
}
}
}

Expand All @@ -1260,7 +1279,13 @@ where
a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<A::Out, RunSystemError>,
b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<B::Out, RunSystemError>,
) -> Result<Self::Out, RunSystemError> {
Ok(a(input, data)? || b(input, data)?)
// `a` might fail to validate against the current world, but if it
// fails, we still want to try running `b`.
match a(input, data) {
// short circuit if `a` is true
Ok(true) => Ok(true),
_ => b(input, data),
}
}
}

Expand All @@ -1282,7 +1307,20 @@ where
a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<A::Out, RunSystemError>,
b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<B::Out, RunSystemError>,
) -> Result<Self::Out, RunSystemError> {
Ok(!(a(input, data)? ^ b(input, data)?))
// even if `a` fails to validate, `b` might succeed
match (a(input, data), b(input, data)) {
(Err(_), Err(_)) => Ok(true),
(Err(e), Ok(v)) | (Ok(v), Err(e)) => {
if !v {
// !(`false` ^ `false`) == true
Ok(true)
} else {
// !(`false` ^ `true`) == `false`, but yield error
Err(e)
}
}
(Ok(a), Ok(b)) => Ok(!(a ^ b)),
}
}
}

Expand All @@ -1304,7 +1342,14 @@ where
a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<A::Out, RunSystemError>,
b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result<B::Out, RunSystemError>,
) -> Result<Self::Out, RunSystemError> {
Ok(a(input, data)? ^ b(input, data)?)
// even if `a` fails to validate, `b` might succeed, in which case XOR
// ought to return true.
match (a(input, data), b(input, data)) {
// arbitrarily return the first error because `a` failed first.
(Err(a), Err(_)) => Err(a),
(Err(_), Ok(v)) | (Ok(v), Err(_)) => Ok(v),
(Ok(a), Ok(b)) => Ok(a ^ b),
}
}
}

Expand All @@ -1313,12 +1358,12 @@ mod tests {
use super::{common_conditions::*, SystemCondition};
use crate::error::{BevyError, DefaultErrorHandler, ErrorContext};
use crate::{
change_detection::ResMut,
change_detection::{Res, ResMut},
component::Component,
message::Message,
query::With,
schedule::{IntoScheduleConfigs, Schedule},
system::{IntoSystem, Local, Res, System},
system::{IntoSystem, Local, System},
world::World,
};
use bevy_ecs_macros::{Resource, SystemSet};
Expand Down Expand Up @@ -1366,6 +1411,252 @@ mod tests {
assert_eq!(world.resource::<Counter>().0, 6);
}

#[test]
fn combinators_with_maybe_failing_condition() {
#![allow(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is specifically because I wanted the expected closure to exactly mirror the combinator systems. could do without allowing these but I think it would be less clear what's being tested against.

clippy::nonminimal_bool,
clippy::overly_complex_bool_expr,
reason = "Trailing `|| false` and `&& true` are used in this test to visually remain consistent with the combinators"
)]

use crate::system::RunSystemOnce;
use alloc::sync::Arc;
use std::sync::Mutex;

// Things that should be tested:
// - the final result of the combinator is correct
// - the systems that are expected to run do run
// - the systems that are expected to not run do not run

#[derive(Component)]
struct Vacant;

// SystemConditions don't have mutable access to the world, so we use a
// `Res<AtomicCounter>` to count invocations.
#[derive(Resource, Default)]
struct Counter(Arc<Mutex<usize>>);

// The following constants are used to represent a system having run.
// both are prime so that when multiplied they give a unique value for any TRUE^n*FALSE^m
const FALSE: usize = 2;
const TRUE: usize = 3;

// this is a system, but has the same side effect as `test_true`
fn is_true_inc(counter: Res<Counter>) -> bool {
test_true(&counter)
}

// this is a system, but has the same side effect as `test_false`
fn is_false_inc(counter: Res<Counter>) -> bool {
test_false(&counter)
}

// This condition will always yield `false`, because `Vacant` is never present.
fn vacant(_: crate::system::Single<&Vacant>) -> bool {
true
}

fn test_true(counter: &Counter) -> bool {
*counter.0.lock().unwrap() *= TRUE;
true
}

fn test_false(counter: &Counter) -> bool {
*counter.0.lock().unwrap() *= FALSE;
false
}

// Helper function that runs a logic call and returns the result, as
// well as the composite number of the calls.
fn logic_call_result(f: impl FnOnce(&Counter) -> bool) -> (usize, bool) {
let counter = Counter(Arc::new(Mutex::new(1)));
let result = f(&counter);
(*counter.0.lock().unwrap(), result)
}

// `test_true` and `test_false` can't fail like the systems can, and so
// we use them to model the short circuiting behavior of rust's logical
// operators. The goal is to end up with a composite number that
// describes rust's behavior and compare that to the result of the
// combinators.

// we expect `true() || false()` to yield `true`, and short circuit
// after `true()`
assert_eq!(
logic_call_result(|c| test_true(c) || test_false(c)),
(TRUE.pow(1) * FALSE.pow(0), true)
);

let mut world = World::new();
world.init_resource::<Counter>();

// ensure there are no `Vacant` entities
assert!(world.query::<&Vacant>().iter(&world).next().is_none());
assert!(matches!(
world.run_system_once((|| true).or(vacant)),
Ok(true)
));

// This system should fail
assert!(RunSystemOnce::run_system_once(&mut world, vacant).is_err());

#[track_caller]
fn assert_system<Marker>(
world: &mut World,
system: impl IntoSystem<(), bool, Marker>,
equivalent_to: impl FnOnce(&Counter) -> bool,
) {
use crate::system::System;

*world.resource::<Counter>().0.lock().unwrap() = 1;

let system = IntoSystem::into_system(system);
let name = system.name();

let out = RunSystemOnce::run_system_once(&mut *world, system).unwrap_or(false);

let (expected_counter, expected) = logic_call_result(equivalent_to);
let caller = core::panic::Location::caller();
let counter = *world.resource::<Counter>().0.lock().unwrap();

assert_eq!(
out,
expected,
"At {}:{} System `{name}` yielded unexpected value `{out}`, expected `{expected}`",
caller.file(),
caller.line(),
);

assert_eq!(
counter, expected_counter,
"At {}:{} System `{name}` did not increment counter as expected: expected `{expected_counter}`, got `{counter}`",
caller.file(),
caller.line(),
);
}

assert_system(&mut world, is_true_inc.or(vacant), |c| {
test_true(c) || false
});
assert_system(&mut world, is_true_inc.nor(vacant), |c| {
!(test_true(c) || false)
});
assert_system(&mut world, is_true_inc.xor(vacant), |c| {
test_true(c) ^ false
});
assert_system(&mut world, is_true_inc.xnor(vacant), |c| {
!(test_true(c) ^ false)
});
assert_system(&mut world, is_true_inc.and(vacant), |c| {
test_true(c) && false
});
assert_system(&mut world, is_true_inc.nand(vacant), |c| {
!(test_true(c) && false)
});

// even if `vacant` fails as the first condition, where applicable (or,
// xor), `is_true_inc` should still be called. `and` and `nand` short
// circuit on an initial `false`.
assert_system(&mut world, vacant.or(is_true_inc), |c| {
false || test_true(c)
});
assert_system(&mut world, vacant.nor(is_true_inc), |c| {
!(false || test_true(c))
});
assert_system(&mut world, vacant.xor(is_true_inc), |c| {
false ^ test_true(c)
});
assert_system(&mut world, vacant.xnor(is_true_inc), |c| {
!(false ^ test_true(c))
});
assert_system(&mut world, vacant.and(is_true_inc), |c| {
false && test_true(c)
});
assert_system(&mut world, vacant.nand(is_true_inc), |c| {
!(false && test_true(c))
});

// the same logic ought to be the case with a condition that runs, but yields `false`:
assert_system(&mut world, is_true_inc.or(is_false_inc), |c| {
test_true(c) || test_false(c)
});
assert_system(&mut world, is_true_inc.nor(is_false_inc), |c| {
!(test_true(c) || test_false(c))
});
assert_system(&mut world, is_true_inc.xor(is_false_inc), |c| {
test_true(c) ^ test_false(c)
});
assert_system(&mut world, is_true_inc.xnor(is_false_inc), |c| {
!(test_true(c) ^ test_false(c))
});
assert_system(&mut world, is_true_inc.and(is_false_inc), |c| {
test_true(c) && test_false(c)
});
assert_system(&mut world, is_true_inc.nand(is_false_inc), |c| {
!(test_true(c) && test_false(c))
});

// and where one condition yields `false` and the other fails:
assert_system(&mut world, is_false_inc.or(vacant), |c| {
test_false(c) || false
});
assert_system(&mut world, is_false_inc.nor(vacant), |c| {
!(test_false(c) || false)
});
assert_system(&mut world, is_false_inc.xor(vacant), |c| {
test_false(c) ^ false
});
assert_system(&mut world, is_false_inc.xnor(vacant), |c| {
!(test_false(c) ^ false)
});
assert_system(&mut world, is_false_inc.and(vacant), |c| {
test_false(c) && false
});
assert_system(&mut world, is_false_inc.nand(vacant), |c| {
!(test_false(c) && false)
});

// and where both conditions yield `true`:
assert_system(&mut world, is_true_inc.or(is_true_inc), |c| {
test_true(c) || test_true(c)
});
assert_system(&mut world, is_true_inc.nor(is_true_inc), |c| {
!(test_true(c) || test_true(c))
});
assert_system(&mut world, is_true_inc.xor(is_true_inc), |c| {
test_true(c) ^ test_true(c)
});
assert_system(&mut world, is_true_inc.xnor(is_true_inc), |c| {
!(test_true(c) ^ test_true(c))
});
assert_system(&mut world, is_true_inc.and(is_true_inc), |c| {
test_true(c) && test_true(c)
});
assert_system(&mut world, is_true_inc.nand(is_true_inc), |c| {
!(test_true(c) && test_true(c))
});

// and where both conditions yield `false`:
assert_system(&mut world, is_false_inc.or(is_false_inc), |c| {
test_false(c) || test_false(c)
});
assert_system(&mut world, is_false_inc.nor(is_false_inc), |c| {
!(test_false(c) || test_false(c))
});
assert_system(&mut world, is_false_inc.xor(is_false_inc), |c| {
test_false(c) ^ test_false(c)
});
assert_system(&mut world, is_false_inc.xnor(is_false_inc), |c| {
!(test_false(c) ^ test_false(c))
});
assert_system(&mut world, is_false_inc.and(is_false_inc), |c| {
test_false(c) && test_false(c)
});
assert_system(&mut world, is_false_inc.nand(is_false_inc), |c| {
!(test_false(c) && test_false(c))
});
}

#[test]
fn run_condition_combinators() {
let mut world = World::new();
Expand Down
19 changes: 9 additions & 10 deletions crates/bevy_ecs/src/system/combinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,10 @@ where
// passed to a function as an unbound non-'static generic argument, they can never be called in parallel
// or re-entrantly because that would require forging another instance of `PrivateUnsafeWorldCell`.
// This means that the world accesses in the two closures will not conflict with each other.
|input, world| unsafe { self.a.run_unsafe(input, world.0) },
// `Self::validate_param_unsafe` already validated the first system,
// but we still need to validate the second system once the first one runs.
|input, world| unsafe {
self.a.validate_param_unsafe(world.0)?;
self.a.run_unsafe(input, world.0)
},
// SAFETY: See the comment above.
|input, world| unsafe {
self.b.validate_param_unsafe(world.0)?;
Expand Down Expand Up @@ -200,14 +201,12 @@ where
#[inline]
unsafe fn validate_param_unsafe(
&mut self,
world: UnsafeWorldCell,
_world: UnsafeWorldCell,
) -> Result<(), SystemParamValidationError> {
// We only validate parameters for the first system,
// since it may make changes to the world that affect
// whether the second system has valid parameters.
// The second system will be validated in `Self::run_unsafe`.
// SAFETY: Delegate to other `System` implementations.
unsafe { self.a.validate_param_unsafe(world) }
// Both systems are validated in `Self::run_unsafe`, so that we get the
// chance to run the second system even if the first one fails to
// validate.
Ok(())
}

fn initialize(&mut self, world: &mut World) -> FilteredAccessSet {
Expand Down
Loading
Loading