Skip to content

Commit fe32a60

Browse files
committed
[assets] set LoadState properly and more testing! (#2226)
1) Sets `LoadState` properly on all failing cases in `AssetServer::load_async` 2) Adds more tests for sad and happy paths of asset loading _Note_: this brings in the `tempfile` crate.
1 parent c2722f7 commit fe32a60

File tree

3 files changed

+215
-26
lines changed

3 files changed

+215
-26
lines changed

crates/bevy_asset/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,7 @@ js-sys = "0.3"
4545

4646
[target.'cfg(target_os = "android")'.dependencies]
4747
ndk-glue = { version = "0.2" }
48+
49+
[dev-dependencies]
50+
futures-lite = "1.4.0"
51+
tempfile = "3.2.0"

crates/bevy_asset/src/asset_server.rs

Lines changed: 204 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ impl AssetServer {
205205
}
206206
LoadState::Failed => return LoadState::Failed,
207207
LoadState::NotLoaded => return LoadState::NotLoaded,
208+
LoadState::Unloaded => return LoadState::Unloaded,
208209
},
209210
HandleId::Id(_, _) => return LoadState::NotLoaded,
210211
}
@@ -230,7 +231,6 @@ impl AssetServer {
230231
self.load_untyped(path).typed()
231232
}
232233

233-
// TODO: properly set failed LoadState in all failure cases
234234
async fn load_async(
235235
&self,
236236
asset_path: AssetPath<'_>,
@@ -272,15 +272,19 @@ impl AssetServer {
272272
source_info.version
273273
};
274274

275+
let set_asset_failed = || {
276+
let mut asset_sources = self.server.asset_sources.write();
277+
let source_info = asset_sources
278+
.get_mut(&asset_path_id.source_path_id())
279+
.expect("`AssetSource` should exist at this point.");
280+
source_info.load_state = LoadState::Failed;
281+
};
282+
275283
// load the asset bytes
276284
let bytes = match self.server.asset_io.load_path(asset_path.path()).await {
277285
Ok(bytes) => bytes,
278286
Err(err) => {
279-
let mut asset_sources = self.server.asset_sources.write();
280-
let source_info = asset_sources
281-
.get_mut(&asset_path_id.source_path_id())
282-
.expect("`AssetSource` should exist at this point.");
283-
source_info.load_state = LoadState::Failed;
287+
set_asset_failed();
284288
return Err(AssetServerError::AssetIoError(err));
285289
}
286290
};
@@ -293,10 +297,15 @@ impl AssetServer {
293297
version,
294298
&self.server.task_pool,
295299
);
296-
asset_loader
300+
301+
if let Err(err) = asset_loader
297302
.load(&bytes, &mut load_context)
298303
.await
299-
.map_err(AssetServerError::AssetLoaderError)?;
304+
.map_err(AssetServerError::AssetLoaderError)
305+
{
306+
set_asset_failed();
307+
return Err(err);
308+
}
300309

301310
// if version has changed since we loaded and grabbed a lock, return. theres is a newer
302311
// version being loaded
@@ -500,9 +509,7 @@ impl AssetServer {
500509
.get_or_insert_with(|| self.server.asset_sources.write());
501510
if let Some(source_info) = asset_sources.get_mut(&id.source_path_id()) {
502511
source_info.committed_assets.remove(&id.label_id());
503-
if source_info.is_loaded() {
504-
source_info.load_state = LoadState::Loaded;
505-
}
512+
source_info.load_state = LoadState::Unloaded;
506513
}
507514
}
508515
assets.remove(handle_id);
@@ -516,23 +523,35 @@ impl AssetServer {
516523
}
517524
}
518525

519-
pub fn free_unused_assets_system(asset_server: Res<AssetServer>) {
526+
fn free_unused_assets_system_impl(asset_server: &AssetServer) {
520527
asset_server.free_unused_assets();
521528
asset_server.mark_unused_assets();
522529
}
523530

531+
pub fn free_unused_assets_system(asset_server: Res<AssetServer>) {
532+
free_unused_assets_system_impl(&asset_server);
533+
}
534+
524535
#[cfg(test)]
525536
mod test {
526537
use super::*;
538+
use crate::{loader::LoadedAsset, update_asset_storage_system};
539+
use bevy_ecs::prelude::*;
540+
use bevy_reflect::TypeUuid;
527541
use bevy_utils::BoxedFuture;
528542

543+
#[derive(Debug, TypeUuid)]
544+
#[uuid = "a5189b72-0572-4290-a2e0-96f73a491c44"]
545+
struct PngAsset;
546+
529547
struct FakePngLoader;
530548
impl AssetLoader for FakePngLoader {
531549
fn load<'a>(
532550
&'a self,
533551
_: &'a [u8],
534-
_: &'a mut LoadContext,
552+
ctx: &'a mut LoadContext,
535553
) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
554+
ctx.set_default_asset(LoadedAsset::new(PngAsset));
536555
Box::pin(async move { Ok(()) })
537556
}
538557

@@ -541,6 +560,21 @@ mod test {
541560
}
542561
}
543562

563+
struct FailingLoader;
564+
impl AssetLoader for FailingLoader {
565+
fn load<'a>(
566+
&'a self,
567+
_: &'a [u8],
568+
_: &'a mut LoadContext,
569+
) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
570+
Box::pin(async { anyhow::bail!("failed") })
571+
}
572+
573+
fn extensions(&self) -> &[&str] {
574+
&["fail"]
575+
}
576+
}
577+
544578
struct FakeMultipleDotLoader;
545579
impl AssetLoader for FakeMultipleDotLoader {
546580
fn load<'a>(
@@ -556,10 +590,10 @@ mod test {
556590
}
557591
}
558592

559-
fn setup() -> AssetServer {
593+
fn setup(asset_path: impl AsRef<Path>) -> AssetServer {
560594
use crate::FileAssetIo;
561595

562-
let asset_server = AssetServer {
596+
AssetServer {
563597
server: Arc::new(AssetServerInternal {
564598
loaders: Default::default(),
565599
extension_to_loader_index: Default::default(),
@@ -568,38 +602,39 @@ mod test {
568602
handle_to_path: Default::default(),
569603
asset_lifecycles: Default::default(),
570604
task_pool: Default::default(),
571-
asset_io: Box::new(FileAssetIo::new(&".")),
605+
asset_io: Box::new(FileAssetIo::new(asset_path)),
572606
}),
573-
};
574-
asset_server.add_loader::<FakePngLoader>(FakePngLoader);
575-
asset_server.add_loader::<FakeMultipleDotLoader>(FakeMultipleDotLoader);
576-
asset_server
607+
}
577608
}
578609

579610
#[test]
580611
fn extensions() {
581-
let asset_server = setup();
612+
let asset_server = setup(".");
613+
asset_server.add_loader(FakePngLoader);
614+
582615
let t = asset_server.get_path_asset_loader("test.png");
583616
assert_eq!(t.unwrap().extensions()[0], "png");
584617
}
585618

586619
#[test]
587620
fn case_insensitive_extensions() {
588-
let asset_server = setup();
621+
let asset_server = setup(".");
622+
asset_server.add_loader(FakePngLoader);
623+
589624
let t = asset_server.get_path_asset_loader("test.PNG");
590625
assert_eq!(t.unwrap().extensions()[0], "png");
591626
}
592627

593628
#[test]
594629
fn no_loader() {
595-
let asset_server = setup();
630+
let asset_server = setup(".");
596631
let t = asset_server.get_path_asset_loader("test.pong");
597632
assert!(t.is_err());
598633
}
599634

600635
#[test]
601636
fn multiple_extensions_no_loader() {
602-
let asset_server = setup();
637+
let asset_server = setup(".");
603638

604639
assert!(
605640
match asset_server.get_path_asset_loader("test.v1.2.3.pong") {
@@ -634,15 +669,158 @@ mod test {
634669

635670
#[test]
636671
fn filename_with_dots() {
637-
let asset_server = setup();
672+
let asset_server = setup(".");
673+
asset_server.add_loader(FakePngLoader);
674+
638675
let t = asset_server.get_path_asset_loader("test-v1.2.3.png");
639676
assert_eq!(t.unwrap().extensions()[0], "png");
640677
}
641678

642679
#[test]
643680
fn multiple_extensions() {
644-
let asset_server = setup();
681+
let asset_server = setup(".");
682+
asset_server.add_loader(FakeMultipleDotLoader);
683+
645684
let t = asset_server.get_path_asset_loader("test.test.png");
646685
assert_eq!(t.unwrap().extensions()[0], "test.png");
647686
}
687+
688+
fn create_dir_and_file(file: impl AsRef<Path>) -> tempfile::TempDir {
689+
let asset_dir = tempfile::tempdir().unwrap();
690+
std::fs::write(asset_dir.path().join(file), &[]).unwrap();
691+
asset_dir
692+
}
693+
694+
#[test]
695+
fn test_missing_loader() {
696+
let dir = create_dir_and_file("file.not-a-real-extension");
697+
let asset_server = setup(dir.path());
698+
699+
let path: AssetPath = "file.not-a-real-extension".into();
700+
let handle = asset_server.get_handle_untyped(path.get_id());
701+
702+
let err = futures_lite::future::block_on(asset_server.load_async(path.clone(), true))
703+
.unwrap_err();
704+
assert!(match err {
705+
AssetServerError::MissingAssetLoader { extensions } => {
706+
extensions == ["not-a-real-extension"]
707+
}
708+
_ => false,
709+
});
710+
711+
assert_eq!(asset_server.get_load_state(handle), LoadState::NotLoaded);
712+
}
713+
714+
#[test]
715+
fn test_invalid_asset_path() {
716+
let asset_server = setup(".");
717+
asset_server.add_loader(FakePngLoader);
718+
719+
let path: AssetPath = "an/invalid/path.png".into();
720+
let handle = asset_server.get_handle_untyped(path.get_id());
721+
722+
let err = futures_lite::future::block_on(asset_server.load_async(path.clone(), true))
723+
.unwrap_err();
724+
assert!(matches!(err, AssetServerError::AssetIoError(_)));
725+
726+
assert_eq!(asset_server.get_load_state(handle), LoadState::Failed);
727+
}
728+
729+
#[test]
730+
fn test_failing_loader() {
731+
let dir = create_dir_and_file("fake.fail");
732+
let asset_server = setup(dir.path());
733+
asset_server.add_loader(FailingLoader);
734+
735+
let path: AssetPath = "fake.fail".into();
736+
let handle = asset_server.get_handle_untyped(path.get_id());
737+
738+
let err = futures_lite::future::block_on(asset_server.load_async(path.clone(), true))
739+
.unwrap_err();
740+
assert!(matches!(err, AssetServerError::AssetLoaderError(_)));
741+
742+
assert_eq!(asset_server.get_load_state(handle), LoadState::Failed);
743+
}
744+
745+
#[test]
746+
fn test_asset_lifecycle() {
747+
let dir = create_dir_and_file("fake.png");
748+
let asset_server = setup(dir.path());
749+
asset_server.add_loader(FakePngLoader);
750+
let assets = asset_server.register_asset_type::<PngAsset>();
751+
752+
let mut world = World::new();
753+
world.insert_resource(assets);
754+
world.insert_resource(asset_server);
755+
756+
let mut tick = {
757+
let mut free_unused_assets_system = free_unused_assets_system.system();
758+
free_unused_assets_system.initialize(&mut world);
759+
let mut update_asset_storage_system = update_asset_storage_system::<PngAsset>.system();
760+
update_asset_storage_system.initialize(&mut world);
761+
762+
move |world: &mut World| {
763+
free_unused_assets_system.run((), world);
764+
update_asset_storage_system.run((), world);
765+
}
766+
};
767+
768+
fn load_asset(path: AssetPath, world: &World) -> HandleUntyped {
769+
let asset_server = world.get_resource::<AssetServer>().unwrap();
770+
let id = futures_lite::future::block_on(asset_server.load_async(path.clone(), true))
771+
.unwrap();
772+
asset_server.get_handle_untyped(id)
773+
}
774+
775+
fn get_asset(id: impl Into<HandleId>, world: &World) -> Option<&PngAsset> {
776+
world
777+
.get_resource::<Assets<PngAsset>>()
778+
.unwrap()
779+
.get(id.into())
780+
}
781+
782+
fn get_load_state(id: impl Into<HandleId>, world: &World) -> LoadState {
783+
world
784+
.get_resource::<AssetServer>()
785+
.unwrap()
786+
.get_load_state(id.into())
787+
}
788+
789+
// ---
790+
// Start of the actual lifecycle test
791+
// ---
792+
793+
let path: AssetPath = "fake.png".into();
794+
assert_eq!(LoadState::NotLoaded, get_load_state(path.get_id(), &world));
795+
796+
// load the asset
797+
let handle = load_asset(path.clone(), &world);
798+
let weak_handle = handle.clone_weak();
799+
800+
// asset is loading
801+
assert_eq!(LoadState::Loading, get_load_state(&handle, &world));
802+
803+
tick(&mut world);
804+
// asset should exist and be loaded at this point
805+
assert_eq!(LoadState::Loaded, get_load_state(&handle, &world));
806+
assert!(get_asset(&handle, &world).is_some());
807+
808+
// after dropping the handle, next call to `tick` will prepare the assets for removal.
809+
drop(handle);
810+
tick(&mut world);
811+
assert_eq!(LoadState::Loaded, get_load_state(&weak_handle, &world));
812+
assert!(get_asset(&weak_handle, &world).is_some());
813+
814+
// second call to tick will actually remove the asset.
815+
tick(&mut world);
816+
assert_eq!(LoadState::Unloaded, get_load_state(&weak_handle, &world));
817+
assert!(get_asset(&weak_handle, &world).is_none());
818+
819+
// finally, reload the asset
820+
let handle = load_asset(path.clone(), &world);
821+
assert_eq!(LoadState::Loading, get_load_state(&handle, &world));
822+
tick(&mut world);
823+
assert_eq!(LoadState::Loaded, get_load_state(&handle, &world));
824+
assert!(get_asset(&handle, &world).is_some());
825+
}
648826
}

crates/bevy_asset/src/info.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@ impl SourceInfo {
4141
/// The load state of an asset
4242
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
4343
pub enum LoadState {
44+
/// The asset has not be loaded.
4445
NotLoaded,
46+
/// The asset in the the process of loading.
4547
Loading,
48+
/// The asset has loaded and is living inside an [`Assets`](crate::Assets) collection.
4649
Loaded,
50+
/// The asset failed to load.
4751
Failed,
52+
/// The asset was previously loaded, however all handles were dropped and
53+
/// the asset was removed from the [`Assets`](crate::Assets) collection.
54+
Unloaded,
4855
}

0 commit comments

Comments
 (0)