Skip to content

Commit 64199f8

Browse files
Add two basic tests for asset hot reloading. (#21203)
# Objective - Improve testing of `bevy_asset`. ## Solution - Add a simple test for hot reloading, as well as a helper function for testing hot reloading in general. - Add an ignored test which should be fixed by #21183. ## Testing - :) --------- Co-authored-by: Alice Cecile <[email protected]>
1 parent 180307c commit 64199f8

File tree

2 files changed

+172
-2
lines changed

2 files changed

+172
-2
lines changed

crates/bevy_asset/src/lib.rs

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,8 @@ mod tests {
706706
io::{
707707
gated::{GateOpener, GatedReader},
708708
memory::{Dir, MemoryAssetReader},
709-
AssetReader, AssetReaderError, AssetSource, AssetSourceId, Reader,
709+
AssetReader, AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId,
710+
AssetWatcher, Reader,
710711
},
711712
loader::{AssetLoader, LoadContext},
712713
Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
@@ -730,8 +731,9 @@ mod tests {
730731
use bevy_platform::collections::{HashMap, HashSet};
731732
use bevy_reflect::TypePath;
732733
use core::time::Duration;
734+
use crossbeam_channel::Sender;
733735
use serde::{Deserialize, Serialize};
734-
use std::path::Path;
736+
use std::path::{Path, PathBuf};
735737
use thiserror::Error;
736738

737739
#[derive(Asset, TypePath, Debug, Default)]
@@ -2064,4 +2066,167 @@ mod tests {
20642066
Err(InvalidGenerationError::Removed { index })
20652067
);
20662068
}
2069+
2070+
// Creates a basic app with the default asset source engineered to get back the asset event
2071+
// sender.
2072+
fn create_app_with_source_event_sender() -> (App, Dir, Sender<AssetSourceEvent>) {
2073+
let mut app = App::new();
2074+
let dir = Dir::default();
2075+
let memory_reader = MemoryAssetReader { root: dir.clone() };
2076+
2077+
// Create a channel to pass the source event sender back to us.
2078+
let (sender_sender, sender_receiver) = crossbeam_channel::bounded(1);
2079+
2080+
struct FakeWatcher;
2081+
impl AssetWatcher for FakeWatcher {}
2082+
2083+
app.register_asset_source(
2084+
AssetSourceId::Default,
2085+
AssetSource::build()
2086+
.with_reader(move || Box::new(memory_reader.clone()))
2087+
.with_watcher(move |sender| {
2088+
sender_sender.send(sender).unwrap();
2089+
Some(Box::new(FakeWatcher))
2090+
}),
2091+
)
2092+
.add_plugins((
2093+
TaskPoolPlugin::default(),
2094+
AssetPlugin {
2095+
watch_for_changes_override: Some(true),
2096+
..Default::default()
2097+
},
2098+
));
2099+
2100+
let sender = sender_receiver.try_recv().unwrap();
2101+
2102+
(app, dir, sender)
2103+
}
2104+
2105+
fn collect_asset_events<A: Asset>(world: &mut World) -> Vec<AssetEvent<A>> {
2106+
world
2107+
.resource_mut::<Messages<AssetEvent<A>>>()
2108+
.drain()
2109+
.collect()
2110+
}
2111+
2112+
fn collect_asset_load_failed_events<A: Asset>(
2113+
world: &mut World,
2114+
) -> Vec<AssetLoadFailedEvent<A>> {
2115+
world
2116+
.resource_mut::<Messages<AssetLoadFailedEvent<A>>>()
2117+
.drain()
2118+
.collect()
2119+
}
2120+
2121+
#[test]
2122+
fn reloads_asset_after_source_event() {
2123+
let (mut app, dir, source_events) = create_app_with_source_event_sender();
2124+
let asset_server = app.world().resource::<AssetServer>().clone();
2125+
2126+
dir.insert_asset_text(
2127+
Path::new("abc.cool.ron"),
2128+
r#"(
2129+
text: "a",
2130+
dependencies: [],
2131+
embedded_dependencies: [],
2132+
sub_texts: [],
2133+
)"#,
2134+
);
2135+
2136+
app.init_asset::<CoolText>()
2137+
.init_asset::<SubText>()
2138+
.register_asset_loader(CoolTextLoader);
2139+
2140+
let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2141+
run_app_until(&mut app, |world| {
2142+
let messages = collect_asset_events(world);
2143+
if messages.is_empty() {
2144+
return None;
2145+
}
2146+
assert_eq!(
2147+
messages,
2148+
[
2149+
AssetEvent::LoadedWithDependencies { id: handle.id() },
2150+
AssetEvent::Added { id: handle.id() },
2151+
]
2152+
);
2153+
Some(())
2154+
});
2155+
2156+
// Sending an asset event should result in the asset being reloaded - resulting in a
2157+
// "Modified" message.
2158+
source_events
2159+
.send(AssetSourceEvent::ModifiedAsset(PathBuf::from(
2160+
"abc.cool.ron",
2161+
)))
2162+
.unwrap();
2163+
2164+
run_app_until(&mut app, |world| {
2165+
let messages = collect_asset_events(world);
2166+
if messages.is_empty() {
2167+
return None;
2168+
}
2169+
assert_eq!(
2170+
messages,
2171+
[
2172+
AssetEvent::LoadedWithDependencies { id: handle.id() },
2173+
AssetEvent::Modified { id: handle.id() }
2174+
]
2175+
);
2176+
Some(())
2177+
});
2178+
}
2179+
2180+
#[test]
2181+
fn added_asset_reloads_previously_missing_asset() {
2182+
let (mut app, dir, source_events) = create_app_with_source_event_sender();
2183+
let asset_server = app.world().resource::<AssetServer>().clone();
2184+
2185+
app.init_asset::<CoolText>()
2186+
.init_asset::<SubText>()
2187+
.register_asset_loader(CoolTextLoader);
2188+
2189+
let handle: Handle<CoolText> = asset_server.load("abc.cool.ron");
2190+
run_app_until(&mut app, |world| {
2191+
let failed_ids = collect_asset_load_failed_events(world)
2192+
.drain(..)
2193+
.map(|event| event.id)
2194+
.collect::<Vec<_>>();
2195+
if failed_ids.is_empty() {
2196+
return None;
2197+
}
2198+
assert_eq!(failed_ids, [handle.id()]);
2199+
Some(())
2200+
});
2201+
2202+
// The asset has already been considered as failed to load. Now we add the asset data, and
2203+
// send an AddedAsset event.
2204+
dir.insert_asset_text(
2205+
Path::new("abc.cool.ron"),
2206+
r#"(
2207+
text: "a",
2208+
dependencies: [],
2209+
embedded_dependencies: [],
2210+
sub_texts: [],
2211+
)"#,
2212+
);
2213+
source_events
2214+
.send(AssetSourceEvent::AddedAsset(PathBuf::from("abc.cool.ron")))
2215+
.unwrap();
2216+
2217+
run_app_until(&mut app, |world| {
2218+
let messages = collect_asset_events(world);
2219+
if messages.is_empty() {
2220+
return None;
2221+
}
2222+
assert_eq!(
2223+
messages,
2224+
[
2225+
AssetEvent::LoadedWithDependencies { id: handle.id() },
2226+
AssetEvent::Added { id: handle.id() }
2227+
]
2228+
);
2229+
Some(())
2230+
});
2231+
}
20672232
}

crates/bevy_asset/src/server/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,6 +1763,11 @@ pub fn handle_internal_asset_events(world: &mut World) {
17631763
}
17641764
}
17651765

1766+
// Drop the lock on `AssetInfos` before spawning a task that may block on it in
1767+
// single-threaded.
1768+
#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))]
1769+
drop(infos);
1770+
17661771
for path in paths_to_reload {
17671772
info!("Reloading {path} because it has changed");
17681773
server.reload(path);

0 commit comments

Comments
 (0)