Skip to content

Commit fe96cb6

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

File tree

1 file changed

+319
-5
lines changed

1 file changed

+319
-5
lines changed

crates/bevy_asset/src/lib.rs

Lines changed: 319 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,315 @@ 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+
#[expect(clippy::allow_attributes, reason = "this is only sometimes unused")]
2356+
#[allow(
2357+
unused,
2358+
reason = "We only use this for asset processor tests, which are only compiled with the `multi_threaded` feature."
2359+
)]
2360+
fn new(m: M) -> Self {
2361+
Self(m, PhantomData)
2362+
}
2363+
}
2364+
2365+
impl<M: MutateAsset<A>, A: Asset> AssetTransformer for RootAssetTransformer<M, A> {
2366+
type AssetInput = A;
2367+
type AssetOutput = A;
2368+
type Error = std::io::Error;
2369+
type Settings = ();
2370+
2371+
async fn transform<'a>(
2372+
&'a self,
2373+
mut asset: TransformedAsset<A>,
2374+
_settings: &'a Self::Settings,
2375+
) -> Result<TransformedAsset<A>, Self::Error> {
2376+
self.0.mutate(asset.get_mut());
2377+
Ok(asset)
2378+
}
2379+
}
2380+
2381+
#[cfg(feature = "multi_threaded")]
2382+
use crate::processor::{AssetProcessor, LoadTransformAndSave};
2383+
2384+
// The asset processor currently requires multi_threaded.
2385+
#[cfg(feature = "multi_threaded")]
2386+
#[test]
2387+
fn no_meta_or_default_processor_copies_asset() {
2388+
// Assets without a meta file or a default processor should still be accessible in the
2389+
// processed path. Note: This isn't exactly the desired property - we don't want the assets
2390+
// to be copied to the processed directory. We just want these assets to still be loadable
2391+
// if we no longer have the source directory. This could be done with a symlink instead of a
2392+
// copy.
2393+
2394+
let AppWithProcessor {
2395+
mut app,
2396+
source_dir,
2397+
processed_dir,
2398+
} = create_app_with_asset_processor();
2399+
2400+
let path = Path::new("abc.cool.ron");
2401+
let source_asset = r#"(
2402+
text: "abc",
2403+
dependencies: [],
2404+
embedded_dependencies: [],
2405+
sub_texts: [],
2406+
)"#;
2407+
2408+
source_dir.insert_asset_text(path, source_asset);
2409+
2410+
// Start the app, which also starts the asset processor.
2411+
app.update();
2412+
2413+
// Wait for all processing to finish.
2414+
bevy_tasks::block_on(
2415+
app.world()
2416+
.resource::<AssetProcessor>()
2417+
.data()
2418+
.wait_until_finished(),
2419+
);
2420+
2421+
let processed_asset = processed_dir.get_asset(path).unwrap();
2422+
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
2423+
assert_eq!(processed_asset, source_asset);
2424+
}
2425+
2426+
// The asset processor currently requires multi_threaded.
2427+
#[cfg(feature = "multi_threaded")]
2428+
#[test]
2429+
fn asset_processor_transforms_asset_default_processor() {
2430+
let AppWithProcessor {
2431+
mut app,
2432+
source_dir,
2433+
processed_dir,
2434+
} = create_app_with_asset_processor();
2435+
2436+
struct AddText;
2437+
2438+
impl MutateAsset<CoolText> for AddText {
2439+
fn mutate(&self, text: &mut CoolText) {
2440+
text.text.push_str("_def");
2441+
}
2442+
}
2443+
2444+
type CoolTextProcessor = LoadTransformAndSave<
2445+
CoolTextLoader,
2446+
RootAssetTransformer<AddText, CoolText>,
2447+
CoolTextSaver,
2448+
>;
2449+
app.register_asset_loader(CoolTextLoader)
2450+
.register_asset_processor(CoolTextProcessor::new(
2451+
RootAssetTransformer::new(AddText),
2452+
CoolTextSaver,
2453+
))
2454+
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
2455+
2456+
let path = Path::new("abc.cool.ron");
2457+
source_dir.insert_asset_text(
2458+
path,
2459+
r#"(
2460+
text: "abc",
2461+
dependencies: [],
2462+
embedded_dependencies: [],
2463+
sub_texts: [],
2464+
)"#,
2465+
);
2466+
2467+
// Start the app, which also starts the asset processor.
2468+
app.update();
2469+
2470+
// Wait for all processing to finish.
2471+
bevy_tasks::block_on(
2472+
app.world()
2473+
.resource::<AssetProcessor>()
2474+
.data()
2475+
.wait_until_finished(),
2476+
);
2477+
2478+
let processed_asset = processed_dir.get_asset(path).unwrap();
2479+
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
2480+
assert_eq!(
2481+
processed_asset,
2482+
r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#
2483+
);
2484+
}
2485+
2486+
// The asset processor currently requires multi_threaded.
2487+
#[cfg(feature = "multi_threaded")]
2488+
#[test]
2489+
fn asset_processor_transforms_asset_with_meta() {
2490+
let AppWithProcessor {
2491+
mut app,
2492+
source_dir,
2493+
processed_dir,
2494+
} = create_app_with_asset_processor();
2495+
2496+
struct AddText;
2497+
2498+
impl MutateAsset<CoolText> for AddText {
2499+
fn mutate(&self, text: &mut CoolText) {
2500+
text.text.push_str("_def");
2501+
}
2502+
}
2503+
2504+
type CoolTextProcessor = LoadTransformAndSave<
2505+
CoolTextLoader,
2506+
RootAssetTransformer<AddText, CoolText>,
2507+
CoolTextSaver,
2508+
>;
2509+
app.register_asset_loader(CoolTextLoader)
2510+
.register_asset_processor(CoolTextProcessor::new(
2511+
RootAssetTransformer::new(AddText),
2512+
CoolTextSaver,
2513+
));
2514+
2515+
let path = Path::new("abc.cool.ron");
2516+
source_dir.insert_asset_text(
2517+
path,
2518+
r#"(
2519+
text: "abc",
2520+
dependencies: [],
2521+
embedded_dependencies: [],
2522+
sub_texts: [],
2523+
)"#,
2524+
);
2525+
source_dir.insert_meta_text(path, r#"(
2526+
meta_format_version: "1.0",
2527+
asset: Process(
2528+
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>",
2529+
settings: (
2530+
loader_settings: (),
2531+
transformer_settings: (),
2532+
saver_settings: (),
2533+
),
2534+
),
2535+
)"#);
2536+
2537+
// Start the app, which also starts the asset processor.
2538+
app.update();
2539+
2540+
// Wait for all processing to finish.
2541+
bevy_tasks::block_on(
2542+
app.world()
2543+
.resource::<AssetProcessor>()
2544+
.data()
2545+
.wait_until_finished(),
2546+
);
2547+
2548+
let processed_asset = processed_dir.get_asset(path).unwrap();
2549+
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
2550+
assert_eq!(
2551+
processed_asset,
2552+
r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#
2553+
);
2554+
}
22412555
}

0 commit comments

Comments
 (0)