Repository: NiklasEi/bevy_asset_loader (rust)

Issue:
Add a failure state alongside the success state?
It seems this crate only keeps track of `Loaded` assets, and waits indefinitely if some of the assets actually `Failed` loading. (I drew this conclusion based on the following code. Please correct me if I am wrong.) I don't believe the loading logic could recover after `get_load_state` returns `Failed`, so it seems that there is no point waiting any longer.

https://github.com/NiklasEi/bevy_asset_loader/blob/79d604dde42ccb36f7573bbc95bf22feb9836bc3/bevy_asset_loader/src/loading_state/systems.rs#L77-L83

Rather than waiting for the impossible, it would be great if this crate could also collect all the failures (ideally as asset paths) and transition to a "load failure" state, so the game could do graceful error reporting in this case. I believe there is already enough information for this kind of error reporting via the current `AssetCollection` interface.

I imagine in the new approach, `Loaded` and `Failed` both count as `done` in terms of progress tracking (perhaps also assert against `NotLoaded` and `Unloaded`, because it should be impossible to have such values returned from `get_load_state` in the loading stage), collect all the asset paths for `Failed` assets, and after `done >= total`, transition to either the "success" or the "failure" state according to the number of `Failed` assets. IMO tracking the `Failed` assets should not introduce too much overhead, so it could be the default behaviour (or of course, it could also be behind a feature gate). An alternative approach would be to unconditionally transition to the only "next" state, and let the systems there check the loading status manually by e.g. querying the existence of the `AssetLoadingFailure` resource.

Pull request:
Add failure states
Resolves #79 

- [x] consider to also check for `NotLoaded` and `Unloaded` -> For now we only check for `Failed`. With the other two, I am afraid of race conditions. Both are valid states when the loading hasn't started yet, which happens in a separate async task after calling `load` on the asset server.

Patch:
diff --git a/README.md b/README.md
index 5519dbd..0971c18 100644
--- a/README.md
+++ b/README.md
@@ -340,6 +340,12 @@ When using `stageless` feature, you need to add `progress_tracking_stageless` fe
 
 The loading state runs in a single exclusive system `at_start`. This means that any parallel system in the loading state will always run after all asset handles have been checked for their status. You can thus read the current progress in each frame in a parallel system without worrying about frame lag.
 
+## Failure state
+
+You can configure a failure state in case some asset in a collection fails to load by calling `on_failure_continue_to` with a state (see [`failure_state`](bevy_asset_loader/examples/failure_state.rs) example). If no failure state is configured and some asset fails to load, your application will be stuck in the loading state.
+
+In most cases this happens, an asset file is missing or a certain file ending does not have a corresponding asset loader. In both of these cases the application log should help since Bevy prints warnings about those issues.
+
 ## Usage without a loading state
 
 Although the pattern of a loading state is quite nice, you might have reasons not to use it. In this case `bevy_asset_loader` can still be helpful. Deriving `AssetCollection` on a resource can significantly reduce the boilerplate for managing assets.
diff --git a/bevy_asset_loader/Cargo.toml b/bevy_asset_loader/Cargo.toml
index 67155bf..b2ce122 100644
--- a/bevy_asset_loader/Cargo.toml
+++ b/bevy_asset_loader/Cargo.toml
@@ -81,6 +81,10 @@ name = "progress_tracking"
 path = "examples/progress_tracking.rs"
 required-features = ["progress_tracking", "2d"]
 
+[[example]]
+name = "failure_state"
+path = "examples/failure_state.rs"
+
 [[example]]
 name = "stageless"
 path = "examples/stageless.rs"
@@ -110,6 +114,11 @@ required-features = [
   "progress_tracking", "2d"
 ]
 
+[[example]]
+name = "stageless_failure_state"
+path = "examples/stageless_failure_state.rs"
+required-features = ["stageless"]
+
 [[example]]
 name = "stageless_dynamic_asset"
 path = "examples/stageless_dynamic_asset.rs"
diff --git a/bevy_asset_loader/examples/failure_state.rs b/bevy_asset_loader/examples/failure_state.rs
new file mode 100644
index 0000000..7d564ff
--- /dev/null
+++ b/bevy_asset_loader/examples/failure_state.rs
@@ -0,0 +1,52 @@
+use bevy::app::AppExit;
+use bevy::prelude::*;
+use bevy_asset_loader::prelude::*;
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        .add_loading_state(
+            LoadingState::new(MyStates::AssetLoading)
+                .continue_to_state(MyStates::Next)
+                .on_failure_continue_to_state(MyStates::ErrorScreen)
+                .with_collection::<MyAssets>(),
+        )
+        .add_state(MyStates::AssetLoading)
+        .add_system_set(SystemSet::on_update(MyStates::AssetLoading).with_system(timeout))
+        .add_system_set(SystemSet::on_enter(MyStates::Next).with_system(fail))
+        .add_system_set(SystemSet::on_enter(MyStates::ErrorScreen).with_system(ok))
+        .run();
+}
+
+#[derive(AssetCollection)]
+struct MyAssets {
+    #[asset(path = "audio/plop.ogg")]
+    _plop: Handle<AudioSource>,
+    #[asset(path = "non-existing-file.ogg")]
+    _non_existing_file: Handle<AudioSource>,
+    #[asset(path = "audio/background.ogg")]
+    _background: Handle<AudioSource>,
+}
+
+fn fail() {
+    panic!("The library should have switched to the failure state!");
+}
+
+fn ok(mut quit: EventWriter<AppExit>) {
+    println!("As expected, the library switched to the failure state");
+    println!("Quitting the application...");
+    quit.send(AppExit);
+}
+
+fn timeout(time: Res<Time>) {
+    if time.seconds_since_startup() > 10. {
+        panic!("The asset loader did not change the state in 10 seconds");
+    }
+}
+
+#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+enum MyStates {
+    AssetLoading,
+    Next,
+    ErrorScreen,
+}
diff --git a/bevy_asset_loader/examples/stageless_failure_state.rs b/bevy_asset_loader/examples/stageless_failure_state.rs
new file mode 100644
index 0000000..d9c49a9
--- /dev/null
+++ b/bevy_asset_loader/examples/stageless_failure_state.rs
@@ -0,0 +1,54 @@
+use bevy::app::AppExit;
+use bevy::prelude::*;
+use bevy_asset_loader::prelude::*;
+use iyes_loopless::prelude::*;
+
+fn main() {
+    App::new()
+        .add_loopless_state(MyStates::AssetLoading)
+        .add_plugins(DefaultPlugins)
+        .add_loading_state(
+            LoadingState::new(MyStates::AssetLoading)
+                .continue_to_state(MyStates::Next)
+                .on_failure_continue_to_state(MyStates::ErrorScreen)
+                .with_collection::<MyAssets>(),
+        )
+        .add_state(MyStates::AssetLoading)
+        .add_system(timeout.run_in_state(MyStates::AssetLoading))
+        .add_enter_system(MyStates::Next, fail)
+        .add_enter_system(MyStates::ErrorScreen, ok)
+        .run();
+}
+
+#[derive(AssetCollection)]
+struct MyAssets {
+    #[asset(path = "audio/plop.ogg")]
+    _plop: Handle<AudioSource>,
+    #[asset(path = "non-existing-file.ogg")]
+    _non_existing_file: Handle<AudioSource>,
+    #[asset(path = "audio/background.ogg")]
+    _background: Handle<AudioSource>,
+}
+
+fn fail() {
+    panic!("The library should have switched to the failure state!");
+}
+
+fn ok(mut quit: EventWriter<AppExit>) {
+    println!("As expected, the library switched to the failure state");
+    println!("Quitting the application...");
+    quit.send(AppExit);
+}
+
+fn timeout(time: Res<Time>) {
+    if time.seconds_since_startup() > 10. {
+        panic!("The asset loader did not change the state in 10 seconds");
+    }
+}
+
+#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+enum MyStates {
+    AssetLoading,
+    Next,
+    ErrorScreen,
+}
diff --git a/bevy_asset_loader/src/dynamic_asset.rs b/bevy_asset_loader/src/dynamic_asset.rs
index e7f55da..054bd8c 100644
--- a/bevy_asset_loader/src/dynamic_asset.rs
+++ b/bevy_asset_loader/src/dynamic_asset.rs
@@ -26,7 +26,7 @@ pub trait DynamicAsset: Debug + Send + Sync {
     fn build(&self, world: &mut World) -> Result<DynamicAssetType, anyhow::Error>;
 }
 
-/// Resource to dynamically resolve keys to asset paths.
+/// Resource to dynamically resolve keys to assets.
 ///
 /// This resource is set by a [`LoadingState`](crate::loading_state::LoadingState) and is read when entering the corresponding Bevy [`State`](::bevy::ecs::schedule::State).
 /// If you want to manage your dynamic assets manually, they should be configured in a previous [`State`](::bevy::ecs::schedule::State).
diff --git a/bevy_asset_loader/src/loading_state.rs b/bevy_asset_loader/src/loading_state.rs
index 3df58eb..e61ef0f 100644
--- a/bevy_asset_loader/src/loading_state.rs
+++ b/bevy_asset_loader/src/loading_state.rs
@@ -139,6 +139,7 @@ use crate::dynamic_asset::{DynamicAsset, DynamicAssets};
 /// ```
 pub struct LoadingState<State> {
     next_state: Option<State>,
+    failure_state: Option<State>,
     loading_state: State,
     dynamic_assets: HashMap<String, Box<dyn DynamicAsset>>,
     check_loading_collections: SystemSet,
@@ -204,6 +205,7 @@ where
     pub fn new(load: S) -> LoadingState<S> {
         Self {
             next_state: None,
+            failure_state: None,
             loading_state: load,
             dynamic_assets: HashMap::default(),
             initialize_dependencies: SystemSet::on_exit(InternalLoadingState::Initialize),
@@ -269,6 +271,7 @@ where
     pub fn new(load: S) -> LoadingState<S> {
         Self {
             next_state: None,
+            failure_state: None,
             loading_state: load,
             dynamic_assets: HashMap::default(),
             check_loading_collections: ConditionSet::new()
@@ -335,6 +338,47 @@ where
         self
     }
 
+    /// The [`LoadingState`] will set this Bevy [`State`](::bevy::ecs::schedule::State) if an asset fails to load.
+    /// ```edition2021
+    /// # use bevy_asset_loader::prelude::*;
+    /// # use bevy::prelude::*;
+    /// # use bevy::asset::AssetPlugin;
+    /// # use iyes_loopless::prelude::*;
+    /// # fn main() {
+    ///     App::new()
+    /// #       .add_loopless_state(GameState::Loading)
+    /// #       .add_plugins(MinimalPlugins)
+    /// #       .init_resource::<iyes_progress::ProgressCounter>()
+    /// #       .add_plugin(AssetPlugin::default())
+    ///         .add_loading_state(
+    ///           LoadingState::new(GameState::Loading)
+    ///             .continue_to_state(GameState::Menu)
+    ///             .on_failure_continue_to_state(GameState::Error)
+    ///             .with_collection::<MyAssets>()
+    ///         )
+    /// #       .add_state(GameState::Loading)
+    /// #       .set_runner(|mut app| app.schedule.run(&mut app.world))
+    /// #       .run();
+    /// # }
+    /// # #[derive(Clone, Eq, PartialEq, Debug, Hash)]
+    /// # enum GameState {
+    /// #     Loading,
+    /// #     Error,
+    /// #     Menu
+    /// # }
+    /// # #[derive(AssetCollection)]
+    /// # pub struct MyAssets {
+    /// #     #[asset(path = "audio/background.ogg")]
+    /// #     pub background: Handle<AudioSource>,
+    /// # }
+    /// ```
+    #[must_use]
+    pub fn on_failure_continue_to_state(mut self, next: S) -> Self {
+        self.failure_state = Some(next);
+
+        self
+    }
+
     /// Register files to be loaded as a certain type of [`DynamicAssetCollection`]
     ///
     /// During the loading state, the given dynamic asset collections will be loaded and their
@@ -701,9 +745,12 @@ where
             let mut loading_config = asset_loader_configuration
                 .state_configurations
                 .remove(&self.loading_state)
-                .unwrap_or(LoadingConfiguration::new(self.next_state.clone()));
+                .unwrap_or_default();
             if self.next_state.is_some() {
-                loading_config.next = self.next_state.clone();
+                loading_config.next = self.next_state;
+            }
+            if self.failure_state.is_some() {
+                loading_config.failure = self.failure_state;
             }
             asset_loader_configuration
                 .state_configurations
@@ -865,9 +912,12 @@ where
             let mut loading_config = asset_loader_configuration
                 .state_configurations
                 .remove(&self.loading_state)
-                .unwrap_or_else(|| LoadingConfiguration::new(self.next_state.clone()));
+                .unwrap_or_default();
             if self.next_state.is_some() {
-                loading_config.next = self.next_state.clone();
+                loading_config.next = self.next_state;
+            }
+            if self.failure_state.is_some() {
+                loading_config.failure = self.failure_state;
             }
             asset_loader_configuration
                 .state_configurations
@@ -1046,14 +1096,18 @@ pub(crate) enum InternalLoadingState {
 
 struct LoadingConfiguration<State: StateData> {
     next: Option<State>,
+    failure: Option<State>,
+    loading_failed: bool,
     loading_collections: usize,
     loading_dynamic_collections: usize,
 }
 
-impl<State: StateData> LoadingConfiguration<State> {
-    fn new(state: Option<State>) -> Self {
+impl<State: StateData> Default for LoadingConfiguration<State> {
+    fn default() -> Self {
         LoadingConfiguration {
-            next: state,
+            next: None,
+            failure: None,
+            loading_failed: false,
             loading_collections: 0,
             loading_dynamic_collections: 0,
         }
diff --git a/bevy_asset_loader/src/loading_state/stageless/systems.rs b/bevy_asset_loader/src/loading_state/stageless/systems.rs
index 250fdfe..6cc41a6 100644
--- a/bevy_asset_loader/src/loading_state/stageless/systems.rs
+++ b/bevy_asset_loader/src/loading_state/stageless/systems.rs
@@ -6,6 +6,7 @@ use bevy::ecs::prelude::{FromWorld, World};
 use bevy::ecs::schedule::{Stage, StateData};
 use bevy::ecs::system::{Res, SystemState};
 
+use bevy::log::warn;
 use std::marker::PhantomData;
 
 #[cfg(feature = "progress_tracking")]
@@ -83,6 +84,12 @@ fn count_loaded_handles<S: StateData, Assets: AssetCollection>(
     let asset_server = cell
         .get_resource::<AssetServer>()
         .expect("Cannot get AssetServer resource");
+    let failure = loading_asset_handles
+        .handles
+        .iter()
+        .map(|handle| handle.id)
+        .find(|handle_id| asset_server.get_load_state(*handle_id) == LoadState::Failed)
+        .is_some();
     let done = loading_asset_handles
         .handles
         .iter()
@@ -90,7 +97,7 @@ fn count_loaded_handles<S: StateData, Assets: AssetCollection>(
         .map(|handle_id| asset_server.get_load_state(handle_id))
         .filter(|state| state == &LoadState::Loaded)
         .count();
-    if done < total {
+    if done < total && !failure {
         return Some((done as u32, total as u32));
     }
 
@@ -104,7 +111,13 @@ fn count_loaded_handles<S: StateData, Assets: AssetCollection>(
         .state_configurations
         .get_mut(&state.0)
     {
-        config.loading_collections -= 1;
+        if failure {
+            config.loading_failed = true;
+        } else {
+            config.loading_collections -= 1;
+        }
+    } else {
+        warn!("Failed to read loading state configuration in count_loaded_handles")
     }
 
     Some((done as u32, total as u32))
@@ -123,6 +136,10 @@ pub(crate) fn resume_to_finalize<S: StateData>(
         if configuration.loading_collections == 0 {
             commands.insert_resource(NextState(InternalLoadingState::Finalize))
         }
+        if configuration.loading_failed && configuration.failure.is_some() {
+            let failure = configuration.failure.clone().unwrap();
+            commands.insert_resource(NextState(failure))
+        }
     }
 }
 
diff --git a/bevy_asset_loader/src/loading_state/systems.rs b/bevy_asset_loader/src/loading_state/systems.rs
index ee04190..ec6e3c6 100644
--- a/bevy_asset_loader/src/loading_state/systems.rs
+++ b/bevy_asset_loader/src/loading_state/systems.rs
@@ -1,11 +1,8 @@
 use bevy::asset::{AssetServer, LoadState};
-#[cfg(not(feature = "stageless"))]
-use bevy::ecs::prelude::State;
-use bevy::ecs::prelude::{FromWorld, World};
-use bevy::ecs::schedule::StateData;
+use bevy::ecs::schedule::{State, StateData};
 use bevy::ecs::system::SystemState;
-#[cfg(not(feature = "stageless"))]
-use bevy::ecs::world::WorldCell;
+use bevy::ecs::world::{FromWorld, World, WorldCell};
+use bevy::log::warn;
 use bevy::prelude::{Mut, Res, ResMut, Stage};
 use std::marker::PhantomData;
 
@@ -74,6 +71,12 @@ fn count_loaded_handles<S: StateData, Assets: AssetCollection>(
     let asset_server = cell
         .get_resource::<AssetServer>()
         .expect("Cannot get AssetServer resource");
+    let failure = loading_asset_handles
+        .handles
+        .iter()
+        .map(|handle| handle.id)
+        .find(|handle_id| asset_server.get_load_state(*handle_id) == LoadState::Failed)
+        .is_some();
     let done = loading_asset_handles
         .handles
         .iter()
@@ -81,7 +84,7 @@ fn count_loaded_handles<S: StateData, Assets: AssetCollection>(
         .map(|handle_id| asset_server.get_load_state(handle_id))
         .filter(|state| state == &LoadState::Loaded)
         .count();
-    if done < total {
+    if done < total && !failure {
         return Some((done as u32, total as u32));
     }
 
@@ -95,7 +98,13 @@ fn count_loaded_handles<S: StateData, Assets: AssetCollection>(
         .state_configurations
         .get_mut(state.current())
     {
-        config.loading_collections -= 1;
+        if failure {
+            config.loading_failed = true;
+        } else {
+            config.loading_collections -= 1;
+        }
+    } else {
+        warn!("Failed to read loading state configuration in count_loaded_handles")
     }
 
     Some((done as u32, total as u32))
@@ -104,7 +113,7 @@ fn count_loaded_handles<S: StateData, Assets: AssetCollection>(
 pub(crate) fn resume_to_finalize<S: StateData>(
     loader_configuration: Res<AssetLoaderConfiguration<S>>,
     mut internal_state: ResMut<State<InternalLoadingState>>,
-    user_state: Res<State<S>>,
+    mut user_state: ResMut<State<S>>,
 ) {
     if let Some(configuration) = loader_configuration
         .state_configurations
@@ -113,8 +122,16 @@ pub(crate) fn resume_to_finalize<S: StateData>(
         if configuration.loading_collections == 0 {
             internal_state
                 .overwrite_set(InternalLoadingState::Finalize)
-                .expect("Failed to set internal state to Finalize")
+                .expect("Failed to set internal state to Finalize");
+        }
+        if configuration.loading_failed && configuration.failure.is_some() {
+            let failure = configuration.failure.clone().unwrap();
+            user_state
+                .overwrite_set(failure)
+                .expect("Failed to set loading state to failed");
         }
+    } else {
+        warn!("Failed to read loading state configuration in resume_to_finalize")
     }
 }
 


Test patch:
diff --git a/bevy_asset_loader/tests/continues_to_failure_state.rs b/bevy_asset_loader/tests/continues_to_failure_state.rs
new file mode 100644
index 0000000..65054bc
--- /dev/null
+++ b/bevy_asset_loader/tests/continues_to_failure_state.rs
@@ -0,0 +1,58 @@
+#![allow(dead_code, unused_imports)]
+
+use bevy::app::AppExit;
+use bevy::asset::AssetPlugin;
+use bevy::prelude::*;
+use bevy_asset_loader::asset_collection::AssetCollection;
+use bevy_asset_loader::loading_state::{LoadingState, LoadingStateAppExt};
+
+#[cfg(all(
+    not(feature = "2d"),
+    not(feature = "3d"),
+    not(feature = "progress_tracking"),
+    not(feature = "stageless")
+))]
+#[test]
+fn continues_to_failure_state() {
+    App::new()
+        .add_state(MyStates::Load)
+        .add_plugins(MinimalPlugins)
+        .add_plugin(AssetPlugin::default())
+        .add_loading_state(
+            LoadingState::new(MyStates::Load)
+                .continue_to_state(MyStates::Next)
+                .on_failure_continue_to_state(MyStates::Error)
+                .with_collection::<Audio>(),
+        )
+        .add_system_set(SystemSet::on_update(MyStates::Load).with_system(timeout))
+        .add_system_set(SystemSet::on_enter(MyStates::Next).with_system(fail))
+        .add_system_set(SystemSet::on_enter(MyStates::Error).with_system(exit))
+        .run();
+}
+
+fn fail() {
+    panic!("The library should have switched to the failure state");
+}
+
+fn exit(mut exit: EventWriter<AppExit>) {
+    exit.send(AppExit);
+}
+
+fn timeout(time: Res<Time>) {
+    if time.seconds_since_startup() > 10. {
+        panic!("The asset loader did not change the state in 10 seconds");
+    }
+}
+
+#[derive(AssetCollection)]
+struct Audio {
+    #[asset(path = "audio/plop.ogg")]
+    no_loader_for_ogg_files: Handle<AudioSource>,
+}
+
+#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+enum MyStates {
+    Load,
+    Error,
+    Next,
+}
diff --git a/bevy_asset_loader/tests/multiple_asset_collections.rs b/bevy_asset_loader/tests/multiple_asset_collections.rs
index a101784..3d0a44e 100644
--- a/bevy_asset_loader/tests/multiple_asset_collections.rs
+++ b/bevy_asset_loader/tests/multiple_asset_collections.rs
@@ -51,14 +51,12 @@ fn expect(
     }
 }
 
-#[allow(dead_code)]
 #[derive(AssetCollection)]
 struct PlopAudio {
     #[asset(path = "audio/plop.ogg")]
     plop: Handle<AudioSource>,
 }
 
-#[allow(dead_code)]
 #[derive(AssetCollection)]
 struct BackgroundAudio {
     #[asset(path = "audio/background.ogg")]
diff --git a/bevy_asset_loader/tests/stageless.rs b/bevy_asset_loader/tests/stageless.rs
index 6b47e1a..e090ce5 100644
--- a/bevy_asset_loader/tests/stageless.rs
+++ b/bevy_asset_loader/tests/stageless.rs
@@ -6,6 +6,7 @@
 ))]
 mod stageless {
     mod can_run_without_next_state;
+    mod continues_to_failure_state;
     mod continues_without_collection;
     mod init_resource;
     mod multiple_asset_collections;
diff --git a/bevy_asset_loader/tests/stageless/continues_to_failure_state.rs b/bevy_asset_loader/tests/stageless/continues_to_failure_state.rs
new file mode 100644
index 0000000..65416e7
--- /dev/null
+++ b/bevy_asset_loader/tests/stageless/continues_to_failure_state.rs
@@ -0,0 +1,51 @@
+use bevy::app::AppExit;
+use bevy::asset::AssetPlugin;
+use bevy::prelude::*;
+use bevy_asset_loader::asset_collection::AssetCollection;
+use bevy_asset_loader::loading_state::{LoadingState, LoadingStateAppExt};
+use iyes_loopless::prelude::*;
+
+#[test]
+fn continues_to_failure_state() {
+    App::new()
+        .add_loopless_state(MyStates::Load)
+        .add_plugins(MinimalPlugins)
+        .add_plugin(AssetPlugin::default())
+        .add_loading_state(
+            LoadingState::new(MyStates::Load)
+                .continue_to_state(MyStates::Next)
+                .on_failure_continue_to_state(MyStates::Error)
+                .with_collection::<Audio>(),
+        )
+        .add_system(timeout.run_in_state(MyStates::Load))
+        .add_enter_system(MyStates::Next, fail)
+        .add_enter_system(MyStates::Error, exit)
+        .run();
+}
+
+fn fail() {
+    panic!("The library should have switched to the failure state");
+}
+
+fn exit(mut exit: EventWriter<AppExit>) {
+    exit.send(AppExit);
+}
+
+fn timeout(time: Res<Time>) {
+    if time.seconds_since_startup() > 10. {
+        panic!("The asset loader did not change the state in 10 seconds");
+    }
+}
+
+#[derive(AssetCollection)]
+struct Audio {
+    #[asset(path = "audio/plop.ogg")]
+    _no_loader_for_ogg_files: Handle<AudioSource>,
+}
+
+#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+enum MyStates {
+    Load,
+    Error,
+    Next,
+}
diff --git a/bevy_asset_loader/tests/stageless/multiple_asset_collections.rs b/bevy_asset_loader/tests/stageless/multiple_asset_collections.rs
index 9045702..4a77998 100644
--- a/bevy_asset_loader/tests/stageless/multiple_asset_collections.rs
+++ b/bevy_asset_loader/tests/stageless/multiple_asset_collections.rs
@@ -42,18 +42,16 @@ fn expect(
     }
 }
 
-#[allow(dead_code)]
 #[derive(AssetCollection)]
 struct PlopAudio {
     #[asset(path = "audio/plop.ogg")]
-    plop: Handle<AudioSource>,
+    _plop: Handle<AudioSource>,
 }
 
-#[allow(dead_code)]
 #[derive(AssetCollection)]
 struct BackgroundAudio {
     #[asset(path = "audio/background.ogg")]
-    background: Handle<AudioSource>,
+    _background: Handle<AudioSource>,
 }
 
 #[derive(Clone, Eq, PartialEq, Debug, Hash)]