Skip to content

Commit

Permalink
Add tests to asset queries
Browse files Browse the repository at this point in the history
  • Loading branch information
nicopap committed Jun 28, 2022
1 parent 627402f commit 6ceebf2
Showing 1 changed file with 231 additions and 11 deletions.
242 changes: 231 additions & 11 deletions crates/bevy_asset/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ pub struct AssetChangedFetch<'w, T: Asset> {
#[doc(hidden)]
pub struct AssetChangedState<T: Asset> {
inner: ComponentIdState<Handle<T>>,
asset_changed_id: ComponentId,
asset_changes_id: ComponentId,
asset_changes_archetype_id: ArchetypeComponentId,
}

// SAFETY: `ROQueryFetch<Self>` is the same as `QueryFetch<Self>`
Expand All @@ -116,12 +117,18 @@ unsafe impl<T: Asset> WorldQuery for AssetChanged<T> {
impl<T: Asset> FetchState for AssetChangedState<T> {
fn init(world: &mut World) -> Self {
let err_msg = format!("AssetChanges<{ty}> was not registered, yet AssetChanged<{ty}> is used in a query. Please register AssetChanges<{ty}> with app.add_asset::<{ty}>()", ty = std::any::type_name::<T>());
let archetype = world.archetypes().resource();
let componend_id = world
.components()
.get_resource_id(TypeId::of::<AssetChanges<T>>())
.expect(&err_msg);
let asset_changes_archetype_id =
archetype.get_archetype_component_id(componend_id).unwrap();
assert!(archetype.contains(componend_id));
Self {
inner: ComponentIdState::init(world),
asset_changed_id: world
.components()
.get_resource_id(TypeId::of::<AssetChanges<T>>())
.expect(&err_msg),
asset_changes_id: componend_id,
asset_changes_archetype_id,
}
}

Expand Down Expand Up @@ -195,12 +202,7 @@ unsafe impl<'w, T: Asset> Fetch<'w> for AssetChangedFetch<'w, T> {
#[inline]
fn update_component_access(state: &Self::State, access: &mut FilteredAccess<ComponentId>) {
ReadFetch::<Handle<T>>::update_component_access(&state.inner, access);
assert!(
!access.access().has_write(state.asset_changed_id),
"AssetChanged<{ty}> conflicts with ResMut<AssetChanges<{ty}>> param in this system, because it needs read access to that resource. You should remove the ResMut<AssetChanges<{ty}>> parameter or use `Res<AssetChanges<{ty}>>`. Since `AssetChanges` does not expose any mutating methods outside of `bevy_asset` there is no need for mutable access.",
ty= std::any::type_name::<T>(),
);
access.add_read(state.asset_changed_id);
access.add_read(state.asset_changes_id);
}

#[inline]
Expand All @@ -209,9 +211,227 @@ unsafe impl<'w, T: Asset> Fetch<'w> for AssetChangedFetch<'w, T> {
archetype: &Archetype,
access: &mut Access<ArchetypeComponentId>,
) {
access.add_read(state.asset_changes_archetype_id);
ReadFetch::<Handle<T>>::update_archetype_component_access(&state.inner, archetype, access);
}
}

/// SAFETY: read-only access
unsafe impl<T: Asset> ReadOnlyWorldQuery for AssetChanged<T> {}

#[cfg(test)]
mod tests {
use crate::{AddAsset, Assets};
use bevy_app::{App, AppExit};
use bevy_ecs::{
entity::Entity,
event::EventWriter,
schedule::{IntoSystemDescriptor, ParallelSystemDescriptorCoercion},
system::{Commands, Local, Query, Res, ResMut},
};

use super::*;

#[derive(bevy_reflect::TypeUuid)]
#[uuid = "44115972-f31b-46e5-be5c-2b9aece6a52f"]
struct MyAsset(u32, &'static str);

fn run_app<Params>(system: impl IntoSystemDescriptor<Params>) {
let mut app = App::new();
app.add_plugin(bevy_core::CorePlugin)
.add_plugin(crate::AssetPlugin)
.add_asset::<MyAsset>()
.add_system(system);
app.run();
}

#[test]
#[should_panic]
fn handle_filter_with_other_query_panics() {
fn compatible_filter(
_query1: Query<&mut Handle<MyAsset>>,
_query2: Query<Entity, AssetChanged<MyAsset>>,
) {
println!(
"this line should not run, as &mut Handle<MyAsset> conflicts with AssetChanged<MyAsset>",
);
}
run_app(compatible_filter);
}

#[test]
#[should_panic]
fn handle_query_pos_panics() {
fn incompatible_query(_query: Query<(AssetChanged<MyAsset>, &mut Handle<MyAsset>)>) {
println!(
"this line should not run, as &mut Handle<MyAsset> conflicts with AssetChanged<MyAsset>",
);
}
run_app(incompatible_query);
}

#[test]
#[should_panic]
fn resmut_changes_param_before_panics() {
fn incompatible_params(
_asset: ResMut<AssetChanges<MyAsset>>,
_query: Query<Entity, AssetChanged<MyAsset>>,
) {
println!(
"this line should not run, as AssetChanged<MyAsset> conflicts with ResMut<AssetChanges<MyAsset>>",
);
}
run_app(incompatible_params);
}

#[test]
#[should_panic]
fn resmut_changes_param_after_panics() {
fn incompatible_params(
_query: Query<Entity, AssetChanged<MyAsset>>,
_asset: ResMut<AssetChanges<MyAsset>>,
) {
println!(
"this line should not run, as AssetChanged<MyAsset> conflicts with ResMut<AssetChanges<MyAsset>>",
);
}
run_app(incompatible_params);
}

#[test]
#[should_panic]
fn handle_other_query_panics() {
fn incompatible_params(
_query1: Query<&mut Handle<MyAsset>>,
_query2: Query<AssetChanged<MyAsset>>,
) {
println!(
"this line should not run, as AssetChanged<MyAsset> conflicts with &mut Handle<MyAsset>",
);
}
run_app(incompatible_params);
}

// According to a comment in QueryState::new in bevy_ecs, components on filter
// position shouldn't conflict with components on query position.
#[test]
fn handle_filter_pos_ok() {
fn compatible_filter(
_query: Query<&mut Handle<MyAsset>, AssetChanged<MyAsset>>,
mut exit: EventWriter<AppExit>,
) {
exit.send(AppExit);
}
run_app(compatible_filter);
}

#[test]
fn res_changes_param_ok() {
fn compatible_params(
mut exit: EventWriter<AppExit>,
_asset: Res<AssetChanges<MyAsset>>,
_query: Query<Entity, AssetChanged<MyAsset>>,
) {
exit.send(AppExit);
}
run_app(compatible_params);
}

#[test]
fn asset_change() {
#[derive(Default)]
struct Counter(u32, u32);

let mut app = App::new();

fn count_update(
mut counter: ResMut<Counter>,
assets: Res<Assets<MyAsset>>,
query: Query<&Handle<MyAsset>, AssetChanged<MyAsset>>,
) {
for handle in query.iter() {
let asset = assets.get(handle).unwrap();
let to_update = match asset.0 {
0 => &mut counter.0,
1 => &mut counter.1,
_ => continue,
};
*to_update += 1;
}
}

fn update_once(mut assets: ResMut<Assets<MyAsset>>, mut run_count: Local<u32>) {
let mut update_index = |i| {
let handle = assets
.iter()
.find_map(|(h, a)| (a.0 == i).then(|| h))
.unwrap();
let handle = assets.get_handle(handle);
let mut asset = assets.get_mut(&handle).unwrap();
asset.1 = "new_value";
};
match *run_count {
2 => update_index(0),
3.. => update_index(1),
_ => {}
};
*run_count += 1;
}
app.add_plugin(bevy_core::CorePlugin)
.add_plugin(crate::AssetPlugin)
.add_asset::<MyAsset>()
.init_resource::<Counter>()
.add_startup_system(|mut cmds: Commands, mut assets: ResMut<Assets<MyAsset>>| {
let asset0 = assets.add(MyAsset(0, "init"));
let asset1 = assets.add(MyAsset(1, "init"));
cmds.spawn().insert(asset0.clone());
cmds.spawn().insert(asset0);
cmds.spawn().insert(asset1.clone());
cmds.spawn().insert(asset1.clone());
cmds.spawn().insert(asset1);
})
.add_system(update_once)
.add_system(count_update.before(update_once));

let assert_counter = |app: &App, assert: Counter| {
let counter = app.world.get_resource::<Counter>().unwrap();
assert_eq!(counter.0, assert.0);
assert_eq!(counter.1, assert.1);
};
// First run of the app, `add_startup_system` runs, but not
// asset_event_system, because it is in a stage ran _before_ the startup stage
app.update(); // run_count == 0

// This might be considered a bug, since it asserts a frame delay. So if you happen
// to break test on the following line, consider removing it.
assert_counter(&app, Counter(0, 0));

// Second run, the asset events are emitted in asset_event_system, and the
// AssetChanges res is updated as well.
// This means, our asset_change_counter are updated.
app.update(); // run_count == 1
assert_counter(&app, Counter(2, 3));

// Third run: `run_count` in `update_once` enables it to update
// the `MyAsset` with value 0. However, since last frame no assets
// were updated, asset_change_counter stays the same
app.update(); // run_count == 2
assert_counter(&app, Counter(2, 3));

// Now that `update_once` and asset_event_system ran, the `MyAsset`
// with value 1 got updated. There are two handles linking to it,
// so that does 4 updates total.
// This cycle, update_once updates the assets of index 1, therefore,
// next cycle, we will detect them
app.update(); // run_count == 3
assert_counter(&app, Counter(4, 3));

// Last cycle we updated the asset with 3 associated entities, so
// we got 3 new changes to 1
app.update(); // run_count == 4
assert_counter(&app, Counter(4, 6));
// ibid
app.update(); // run_count == 5
assert_counter(&app, Counter(4, 9));
}
}

0 comments on commit 6ceebf2

Please sign in to comment.