Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
262 changes: 254 additions & 8 deletions crates/bevy_ecs/src/schedule/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ 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)?)
Ok(a(input, data).unwrap_or(false) && b(input, data).unwrap_or(false))
}
}

Expand All @@ -1216,7 +1216,7 @@ 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)?))
Ok(!(a(input, data).unwrap_or(false) && b(input, data).unwrap_or(false)))
}
}

Expand All @@ -1238,7 +1238,7 @@ 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)?))
Ok(!(a(input, data).unwrap_or(false) || b(input, data).unwrap_or(false)))
}
}

Expand All @@ -1260,7 +1260,7 @@ 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)?)
Ok(a(input, data).unwrap_or(false) || b(input, data).unwrap_or(false))
}
}

Expand All @@ -1282,7 +1282,7 @@ 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)?))
Ok(!(a(input, data).unwrap_or(false) ^ b(input, data).unwrap_or(false)))
}
}

Expand All @@ -1304,7 +1304,7 @@ 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)?)
Ok(a(input, data).unwrap_or(false) ^ b(input, data).unwrap_or(false))
}
}

Expand All @@ -1313,12 +1313,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 +1366,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
Loading
Loading