Skip to content
Open
167 changes: 166 additions & 1 deletion crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,7 @@ mod tests {
sync::Mutex,
};
use bevy_reflect::TypePath;
use core::time::Duration;
use core::{any::TypeId, time::Duration};
use futures_lite::AsyncReadExt;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -2906,4 +2906,169 @@ mod tests {
// Now that the dependency is loaded, the subasset is counted as loaded with dependencies!
assert!(asset_server.is_loaded_with_dependencies(&subasset_handle));
}

// A simplified version of `LoadState` for easier comparison.
#[derive(Debug, PartialEq, Eq)]
enum TestLoadState {
NotLoaded,
Loading,
Loaded,
Failed(TestAssetLoadError),
}

// A simplified subset of `AssetLoadError` for easier comparison.
#[derive(Debug, PartialEq, Eq)]
enum TestAssetLoadError {
RequestedHandleTypeMismatch {
requested: TypeId,
actual_asset_name: &'static str,
},
MissingAssetLoader,
AssetReaderErrorNotFound,
AssetLoaderError,
MissingLabel,
}

impl From<LoadState> for TestLoadState {
fn from(value: LoadState) -> Self {
match value {
LoadState::NotLoaded => Self::NotLoaded,
LoadState::Loading => Self::Loading,
LoadState::Loaded => Self::Loaded,
LoadState::Failed(err) => Self::Failed((&*err).into()),
}
}
}

impl From<&AssetLoadError> for TestAssetLoadError {
fn from(value: &AssetLoadError) -> TestAssetLoadError {
match value {
AssetLoadError::RequestedHandleTypeMismatch {
requested,
actual_asset_name,
..
} => Self::RequestedHandleTypeMismatch {
requested: *requested,
actual_asset_name,
},
AssetLoadError::MissingAssetLoader { .. } => Self::MissingAssetLoader,
AssetLoadError::AssetReaderError(AssetReaderError::NotFound(_)) => {
Self::AssetReaderErrorNotFound
}
AssetLoadError::AssetLoaderError { .. } => Self::AssetLoaderError,
AssetLoadError::MissingLabel { .. } => Self::MissingLabel,
_ => panic!("TestAssetLoadError's From<&AssetLoaderError> is missing a case for AssetLoadError \"{:?}\".", value),
}
}
}

// An asset type that doesn't have a registered loader.
#[derive(Asset, TypePath)]
struct LoaderlessAsset;

// Load the given path and test that `AssetServer::get_load_state` returns
// the given state.
fn test_load_state<A: Asset>(
label: &'static str,
path: &'static str,
expected_load_state: TestLoadState,
) {
let (mut app, dir) = create_app();

app.init_asset::<CoolText>()
.init_asset::<SubText>()
.init_asset::<LoaderlessAsset>()
.register_asset_loader(CoolTextLoader);

dir.insert_asset_text(
Path::new("test.cool.ron"),
r#"
(
text: "test",
dependencies: [],
embedded_dependencies: [],
sub_texts: ["subasset"],
)"#,
);

dir.insert_asset_text(Path::new("malformed.cool.ron"), "MALFORMED");

let asset_server = app.world().resource::<AssetServer>().clone();
let handle = asset_server.load::<A>(path);
let mut load_state = TestLoadState::NotLoaded;

for _ in 0..LARGE_ITERATION_COUNT {
app.update();
load_state = asset_server.get_load_state(&handle).unwrap().into();
if load_state == expected_load_state {
break;
}
}
Comment on lines +3000 to +3006
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just use run_app_until here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this initially, but found that it made debugging test failures harder - run_app_until will panic without any information about what failed. Doing it manually lets me print a clear "did X, expected Y, got Z".

I do think there's room for improvement. Maybe adding a run_app_until variant with more options, or even better a way to say "run until all loads have resolved". But don't think that would fit in this PR.


assert!(
load_state == expected_load_state,
"For test \"{}\", expected {:?} but got {:?}.",
label,
expected_load_state,
load_state,
);
}

// Tests that `AssetServer::get_load_state` returns the correct state after
// various loads, some of which trigger errors.
#[test]
fn load_failure() {
test_load_state::<CoolText>("root asset exists", "test.cool.ron", TestLoadState::Loaded);

test_load_state::<SubText>(
"sub-asset exists",
"test.cool.ron#subasset",
TestLoadState::Loaded,
);

test_load_state::<CoolText>(
"root asset does not exist",
"does_not_exist.cool.ron",
TestLoadState::Failed(TestAssetLoadError::AssetReaderErrorNotFound),
);

test_load_state::<CoolText>(
"sub-asset of root asset that does not exist",
"does_not_exist.cool.ron#subasset",
TestLoadState::Failed(TestAssetLoadError::AssetReaderErrorNotFound),
);

test_load_state::<SubText>(
"sub-asset does not exist",
"test.cool.ron#does_not_exist",
TestLoadState::Failed(TestAssetLoadError::MissingLabel),
);

test_load_state::<CoolText>(
"sub-asset is not requested type",
"test.cool.ron#subasset",
TestLoadState::Failed(TestAssetLoadError::RequestedHandleTypeMismatch {
requested: TypeId::of::<CoolText>(),
actual_asset_name: "bevy_asset::tests::SubText",
}),
);

test_load_state::<CoolText>(
"malformed root asset",
"malformed.cool.ron",
TestLoadState::Failed(TestAssetLoadError::AssetLoaderError),
);

test_load_state::<CoolText>(
"sub-asset of malformed root asset",
"malformed.cool.ron#subasset",
TestLoadState::Failed(TestAssetLoadError::AssetLoaderError),
);

test_load_state::<LoaderlessAsset>(
"root asset has no loader",
"loaderless",
TestLoadState::Failed(TestAssetLoadError::MissingAssetLoader),
);
}
}
46 changes: 38 additions & 8 deletions crates/bevy_asset/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -839,19 +839,47 @@ impl AssetServer {
Ok(loaded_asset) => {
let final_handle = if let Some(label) = path.label_cow() {
match loaded_asset.labeled_assets.get(&label) {
Some(labeled_asset) => Some(labeled_asset.handle.clone()),
Some(labeled_asset) => {
// If we know the requested type then check it
// matches the labeled asset.
if let Some(asset_id) = asset_id
&& asset_id.type_id != labeled_asset.handle.type_id()
{
let error = AssetLoadError::RequestedHandleTypeMismatch {
path: path.clone(),
requested: asset_id.type_id,
actual_asset_name: labeled_asset.asset.value.asset_type_name(),
loader_name: loader.type_path(),
};
self.send_asset_event(InternalAssetEvent::Failed {
index: asset_id,
error: error.clone(),
path: path.into_owned(),
});
return Err(error);
}
Some(labeled_asset.handle.clone())
}
None => {
let mut all_labels: Vec<String> = loaded_asset
.labeled_assets
.keys()
.map(|s| (**s).to_owned())
.collect();
all_labels.sort_unstable();
return Err(AssetLoadError::MissingLabel {
let error = AssetLoadError::MissingLabel {
base_path,
label: label.to_string(),
all_labels,
});
};
if let Some(asset_id) = asset_id {
self.send_asset_event(InternalAssetEvent::Failed {
index: asset_id,
error: error.clone(),
path: path.into_owned(),
});
}
return Err(error);
}
}
} else {
Expand All @@ -865,11 +893,13 @@ impl AssetServer {
Ok(final_handle)
}
Err(err) => {
self.send_asset_event(InternalAssetEvent::Failed {
index: base_asset_id,
error: err.clone(),
path: path.into_owned(),
});
if let Some(asset_id) = asset_id {
self.send_asset_event(InternalAssetEvent::Failed {
index: asset_id,
error: err.clone(),
path: path.into_owned(),
});
}
Err(err)
}
}
Expand Down