Skip to content

Commit fb9e912

Browse files
committed
Create a couple basic tests for the asset processor.
1 parent 542815f commit fb9e912

File tree

1 file changed

+318
-5
lines changed

1 file changed

+318
-5
lines changed

crates/bevy_asset/src/lib.rs

Lines changed: 318 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -711,14 +711,16 @@ mod tests {
711711
handle::Handle,
712712
io::{
713713
gated::{GateOpener, GatedReader},
714-
memory::{Dir, MemoryAssetReader},
714+
memory::{Dir, MemoryAssetReader, MemoryAssetWriter},
715715
AssetReader, AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId,
716716
AssetWatcher, Reader,
717717
},
718718
loader::{AssetLoader, LoadContext},
719-
Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
720-
AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, UnapprovedPathMode,
721-
UntypedHandle,
719+
saver::AssetSaver,
720+
transformer::{AssetTransformer, TransformedAsset},
721+
Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetMode,
722+
AssetPath, AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState,
723+
UnapprovedPathMode, UntypedHandle,
722724
};
723725
use alloc::{
724726
boxed::Box,
@@ -739,8 +741,9 @@ mod tests {
739741
sync::Mutex,
740742
};
741743
use bevy_reflect::TypePath;
742-
use core::time::Duration;
744+
use core::{marker::PhantomData, time::Duration};
743745
use crossbeam_channel::Sender;
746+
use futures_lite::AsyncWriteExt;
744747
use serde::{Deserialize, Serialize};
745748
use std::path::{Path, PathBuf};
746749
use thiserror::Error;
@@ -2238,4 +2241,314 @@ mod tests {
22382241
Some(())
22392242
});
22402243
}
2244+
2245+
#[expect(clippy::allow_attributes, reason = "this is only sometimes unused")]
2246+
#[allow(
2247+
unused,
2248+
reason = "We only use this for asset processor tests, which are only compiled with the `multi_threaded` feature."
2249+
)]
2250+
struct AppWithProcessor {
2251+
app: App,
2252+
source_dir: Dir,
2253+
processed_dir: Dir,
2254+
}
2255+
2256+
#[expect(clippy::allow_attributes, reason = "this is only sometimes unused")]
2257+
#[allow(
2258+
unused,
2259+
reason = "We only use this for asset processor tests, which are only compiled with the `multi_threaded` feature."
2260+
)]
2261+
fn create_app_with_asset_processor() -> AppWithProcessor {
2262+
let mut app = App::new();
2263+
let source_dir = Dir::default();
2264+
let processed_dir = Dir::default();
2265+
2266+
let source_memory_reader = MemoryAssetReader {
2267+
root: source_dir.clone(),
2268+
};
2269+
let processed_memory_reader = MemoryAssetReader {
2270+
root: processed_dir.clone(),
2271+
};
2272+
let processed_memory_writer = MemoryAssetWriter {
2273+
root: processed_dir.clone(),
2274+
};
2275+
2276+
app.register_asset_source(
2277+
AssetSourceId::Default,
2278+
AssetSource::build()
2279+
.with_reader(move || Box::new(source_memory_reader.clone()))
2280+
.with_processed_reader(move || Box::new(processed_memory_reader.clone()))
2281+
.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),
2282+
)
2283+
.add_plugins((
2284+
TaskPoolPlugin::default(),
2285+
AssetPlugin {
2286+
mode: AssetMode::Processed,
2287+
use_asset_processor_override: Some(true),
2288+
..Default::default()
2289+
},
2290+
));
2291+
2292+
AppWithProcessor {
2293+
app,
2294+
source_dir,
2295+
processed_dir,
2296+
}
2297+
}
2298+
2299+
#[expect(clippy::allow_attributes, reason = "this is only sometimes unused")]
2300+
#[allow(
2301+
unused,
2302+
reason = "We only use this for asset processor tests, which are only compiled with the `multi_threaded` feature."
2303+
)]
2304+
struct CoolTextSaver;
2305+
2306+
impl AssetSaver for CoolTextSaver {
2307+
type Asset = CoolText;
2308+
type Settings = ();
2309+
type OutputLoader = CoolTextLoader;
2310+
type Error = std::io::Error;
2311+
2312+
async fn save(
2313+
&self,
2314+
writer: &mut crate::io::Writer,
2315+
asset: crate::saver::SavedAsset<'_, Self::Asset>,
2316+
_: &Self::Settings,
2317+
) -> Result<(), Self::Error> {
2318+
let ron = CoolTextRon {
2319+
text: asset.text.clone(),
2320+
sub_texts: asset
2321+
.iter_labels()
2322+
.map(|label| asset.get_labeled::<SubText, _>(label).unwrap().text.clone())
2323+
.collect(),
2324+
dependencies: asset
2325+
.dependencies
2326+
.iter()
2327+
.map(|handle| handle.path().unwrap().path())
2328+
.map(|path| path.to_str().unwrap().to_string())
2329+
.collect(),
2330+
// NOTE: We can't handle embedded dependencies in any way, since we need to write to
2331+
// another file to do so.
2332+
embedded_dependencies: vec![],
2333+
};
2334+
let ron = ron::ser::to_string(&ron).unwrap();
2335+
writer.write_all(ron.as_bytes()).await?;
2336+
Ok(())
2337+
}
2338+
}
2339+
2340+
#[expect(clippy::allow_attributes, reason = "this is only sometimes unused")]
2341+
#[allow(
2342+
unused,
2343+
reason = "We only use this for asset processor tests, which are only compiled with the `multi_threaded` feature."
2344+
)]
2345+
// Note: while we allow any Fn, since closures are unnameable types, creating a processor with a
2346+
// closure cannot be used (since we need to include the name of the transformer in the meta
2347+
// file).
2348+
struct RootAssetTransformer<M: MutateAsset<A>, A: Asset>(M, PhantomData<fn(&mut A)>);
2349+
2350+
trait MutateAsset<A: Asset>: Send + Sync + 'static {
2351+
fn mutate(&self, asset: &mut A);
2352+
}
2353+
2354+
impl<M: MutateAsset<A>, A: Asset> RootAssetTransformer<M, A> {
2355+
#[allow(
2356+
unused,
2357+
reason = "We only use this for asset processor tests, which are only compiled with the `multi_threaded` feature."
2358+
)]
2359+
fn new(m: M) -> Self {
2360+
Self(m, PhantomData)
2361+
}
2362+
}
2363+
2364+
impl<M: MutateAsset<A>, A: Asset> AssetTransformer for RootAssetTransformer<M, A> {
2365+
type AssetInput = A;
2366+
type AssetOutput = A;
2367+
type Error = std::io::Error;
2368+
type Settings = ();
2369+
2370+
async fn transform<'a>(
2371+
&'a self,
2372+
mut asset: TransformedAsset<A>,
2373+
_settings: &'a Self::Settings,
2374+
) -> Result<TransformedAsset<A>, Self::Error> {
2375+
self.0.mutate(asset.get_mut());
2376+
Ok(asset)
2377+
}
2378+
}
2379+
2380+
#[cfg(feature = "multi_threaded")]
2381+
use crate::processor::{AssetProcessor, LoadTransformAndSave};
2382+
2383+
// The asset processor currently requires multi_threaded.
2384+
#[cfg(feature = "multi_threaded")]
2385+
#[test]
2386+
fn no_meta_or_default_processor_copies_asset() {
2387+
// Assets without a meta file or a default processor should still be accessible in the
2388+
// processed path. Note: This isn't exactly the desired property - we don't want the assets
2389+
// to be copied to the processed directory. We just want these assets to still be loadable
2390+
// if we no longer have the source directory. This could be done with a symlink instead of a
2391+
// copy.
2392+
2393+
let AppWithProcessor {
2394+
mut app,
2395+
source_dir,
2396+
processed_dir,
2397+
} = create_app_with_asset_processor();
2398+
2399+
let path = Path::new("abc.cool.ron");
2400+
let source_asset = r#"(
2401+
text: "abc",
2402+
dependencies: [],
2403+
embedded_dependencies: [],
2404+
sub_texts: [],
2405+
)"#;
2406+
2407+
source_dir.insert_asset_text(path, source_asset);
2408+
2409+
// Start the app, which also starts the asset processor.
2410+
app.update();
2411+
2412+
// Wait for all processing to finish.
2413+
bevy_tasks::block_on(
2414+
app.world()
2415+
.resource::<AssetProcessor>()
2416+
.data()
2417+
.wait_until_finished(),
2418+
);
2419+
2420+
let processed_asset = processed_dir.get_asset(path).unwrap();
2421+
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
2422+
assert_eq!(processed_asset, source_asset);
2423+
}
2424+
2425+
// The asset processor currently requires multi_threaded.
2426+
#[cfg(feature = "multi_threaded")]
2427+
#[test]
2428+
fn asset_processor_transforms_asset_default_processor() {
2429+
let AppWithProcessor {
2430+
mut app,
2431+
source_dir,
2432+
processed_dir,
2433+
} = create_app_with_asset_processor();
2434+
2435+
struct AddText;
2436+
2437+
impl MutateAsset<CoolText> for AddText {
2438+
fn mutate(&self, text: &mut CoolText) {
2439+
text.text.push_str("_def");
2440+
}
2441+
}
2442+
2443+
type CoolTextProcessor = LoadTransformAndSave<
2444+
CoolTextLoader,
2445+
RootAssetTransformer<AddText, CoolText>,
2446+
CoolTextSaver,
2447+
>;
2448+
app.register_asset_loader(CoolTextLoader)
2449+
.register_asset_processor(CoolTextProcessor::new(
2450+
RootAssetTransformer::new(AddText),
2451+
CoolTextSaver,
2452+
))
2453+
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
2454+
2455+
let path = Path::new("abc.cool.ron");
2456+
source_dir.insert_asset_text(
2457+
path,
2458+
r#"(
2459+
text: "abc",
2460+
dependencies: [],
2461+
embedded_dependencies: [],
2462+
sub_texts: [],
2463+
)"#,
2464+
);
2465+
2466+
// Start the app, which also starts the asset processor.
2467+
app.update();
2468+
2469+
// Wait for all processing to finish.
2470+
bevy_tasks::block_on(
2471+
app.world()
2472+
.resource::<AssetProcessor>()
2473+
.data()
2474+
.wait_until_finished(),
2475+
);
2476+
2477+
let processed_asset = processed_dir.get_asset(path).unwrap();
2478+
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
2479+
assert_eq!(
2480+
processed_asset,
2481+
r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#
2482+
);
2483+
}
2484+
2485+
// The asset processor currently requires multi_threaded.
2486+
#[cfg(feature = "multi_threaded")]
2487+
#[test]
2488+
fn asset_processor_transforms_asset_with_meta() {
2489+
let AppWithProcessor {
2490+
mut app,
2491+
source_dir,
2492+
processed_dir,
2493+
} = create_app_with_asset_processor();
2494+
2495+
struct AddText;
2496+
2497+
impl MutateAsset<CoolText> for AddText {
2498+
fn mutate(&self, text: &mut CoolText) {
2499+
text.text.push_str("_def");
2500+
}
2501+
}
2502+
2503+
type CoolTextProcessor = LoadTransformAndSave<
2504+
CoolTextLoader,
2505+
RootAssetTransformer<AddText, CoolText>,
2506+
CoolTextSaver,
2507+
>;
2508+
app.register_asset_loader(CoolTextLoader)
2509+
.register_asset_processor(CoolTextProcessor::new(
2510+
RootAssetTransformer::new(AddText),
2511+
CoolTextSaver,
2512+
));
2513+
2514+
let path = Path::new("abc.cool.ron");
2515+
source_dir.insert_asset_text(
2516+
path,
2517+
r#"(
2518+
text: "abc",
2519+
dependencies: [],
2520+
embedded_dependencies: [],
2521+
sub_texts: [],
2522+
)"#,
2523+
);
2524+
source_dir.insert_meta_text(path, r#"(
2525+
meta_format_version: "1.0",
2526+
asset: Process(
2527+
processor: "bevy_asset::processor::process::LoadTransformAndSave<bevy_asset::tests::CoolTextLoader, bevy_asset::tests::RootAssetTransformer<bevy_asset::tests::asset_processor_transforms_asset_with_meta::AddText, bevy_asset::tests::CoolText>, bevy_asset::tests::CoolTextSaver>",
2528+
settings: (
2529+
loader_settings: (),
2530+
transformer_settings: (),
2531+
saver_settings: (),
2532+
),
2533+
),
2534+
)"#);
2535+
2536+
// Start the app, which also starts the asset processor.
2537+
app.update();
2538+
2539+
// Wait for all processing to finish.
2540+
bevy_tasks::block_on(
2541+
app.world()
2542+
.resource::<AssetProcessor>()
2543+
.data()
2544+
.wait_until_finished(),
2545+
);
2546+
2547+
let processed_asset = processed_dir.get_asset(path).unwrap();
2548+
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
2549+
assert_eq!(
2550+
processed_asset,
2551+
r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#
2552+
);
2553+
}
22412554
}

0 commit comments

Comments
 (0)