Path: blob/main/crates/bevy_asset/src/processor/tests.rs
7223 views
use alloc::{1boxed::Box,2collections::BTreeMap,3string::{String, ToString},4vec,5vec::Vec,6};7use bevy_reflect::TypePath;8use core::marker::PhantomData;9use futures_lite::AsyncWriteExt;10use serde::{Deserialize, Serialize};11use std::path::Path;1213use bevy_app::{App, TaskPoolPlugin};14use bevy_ecs::error::BevyError;15use bevy_tasks::BoxedFuture;1617use crate::{18io::{19memory::{Dir, MemoryAssetReader, MemoryAssetWriter},20AssetSource, AssetSourceId, Reader,21},22processor::{23AssetProcessor, LoadTransformAndSave, LogEntry, ProcessorState, ProcessorTransactionLog,24ProcessorTransactionLogFactory,25},26saver::AssetSaver,27tests::{run_app_until, CoolText, CoolTextLoader, CoolTextRon, SubText},28transformer::{AssetTransformer, TransformedAsset},29Asset, AssetApp, AssetLoader, AssetMode, AssetPath, AssetPlugin, LoadContext,30};3132struct AppWithProcessor {33app: App,34source_dir: Dir,35processed_dir: Dir,36}3738fn create_app_with_asset_processor() -> AppWithProcessor {39let mut app = App::new();40let source_dir = Dir::default();41let processed_dir = Dir::default();4243let source_memory_reader = MemoryAssetReader {44root: source_dir.clone(),45};46let processed_memory_reader = MemoryAssetReader {47root: processed_dir.clone(),48};49let processed_memory_writer = MemoryAssetWriter {50root: processed_dir.clone(),51};5253app.register_asset_source(54AssetSourceId::Default,55AssetSource::build()56.with_reader(move || Box::new(source_memory_reader.clone()))57.with_processed_reader(move || Box::new(processed_memory_reader.clone()))58.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),59)60.add_plugins((61TaskPoolPlugin::default(),62AssetPlugin {63mode: AssetMode::Processed,64use_asset_processor_override: Some(true),65..Default::default()66},67));6869/// A dummy transaction log factory that just creates [`FakeTransactionLog`].70struct FakeTransactionLogFactory;7172impl ProcessorTransactionLogFactory for FakeTransactionLogFactory {73fn read(&self) -> BoxedFuture<'_, Result<Vec<LogEntry>, BevyError>> {74Box::pin(async move { Ok(vec![]) })75}7677fn create_new_log(78&self,79) -> BoxedFuture<'_, Result<Box<dyn ProcessorTransactionLog>, BevyError>> {80Box::pin(async move { Ok(Box::new(FakeTransactionLog) as _) })81}82}8384/// A dummy transaction log that just drops every log.85// TODO: In the future it's possible for us to have a test of the transaction log, so making86// this more complex may be necessary.87struct FakeTransactionLog;8889impl ProcessorTransactionLog for FakeTransactionLog {90fn begin_processing<'a>(91&'a mut self,92_asset: &'a AssetPath<'_>,93) -> BoxedFuture<'a, Result<(), BevyError>> {94Box::pin(async move { Ok(()) })95}9697fn end_processing<'a>(98&'a mut self,99_asset: &'a AssetPath<'_>,100) -> BoxedFuture<'a, Result<(), BevyError>> {101Box::pin(async move { Ok(()) })102}103104fn unrecoverable(&mut self) -> BoxedFuture<'_, Result<(), BevyError>> {105Box::pin(async move { Ok(()) })106}107}108109app.world()110.resource::<AssetProcessor>()111.data()112.set_log_factory(Box::new(FakeTransactionLogFactory))113.unwrap();114115AppWithProcessor {116app,117source_dir,118processed_dir,119}120}121122fn run_app_until_finished_processing(app: &mut App) {123run_app_until(app, |world| {124if bevy_tasks::block_on(world.resource::<AssetProcessor>().get_state())125== ProcessorState::Finished126{127Some(())128} else {129None130}131});132}133134struct CoolTextSaver;135136impl AssetSaver for CoolTextSaver {137type Asset = CoolText;138type Settings = ();139type OutputLoader = CoolTextLoader;140type Error = std::io::Error;141142async fn save(143&self,144writer: &mut crate::io::Writer,145asset: crate::saver::SavedAsset<'_, Self::Asset>,146_: &Self::Settings,147) -> Result<(), Self::Error> {148let ron = CoolTextRon {149text: asset.text.clone(),150sub_texts: asset151.iter_labels()152.map(|label| asset.get_labeled::<SubText, _>(label).unwrap().text.clone())153.collect(),154dependencies: asset155.dependencies156.iter()157.map(|handle| handle.path().unwrap().path())158.map(|path| path.to_str().unwrap().to_string())159.collect(),160// NOTE: We can't handle embedded dependencies in any way, since we need to write to161// another file to do so.162embedded_dependencies: vec![],163};164let ron = ron::ser::to_string(&ron).unwrap();165writer.write_all(ron.as_bytes()).await?;166Ok(())167}168}169170// Note: while we allow any Fn, since closures are unnameable types, creating a processor with a171// closure cannot be used (since we need to include the name of the transformer in the meta172// file).173struct RootAssetTransformer<M: MutateAsset<A>, A: Asset>(M, PhantomData<fn(&mut A)>);174175trait MutateAsset<A: Asset>: Send + Sync + 'static {176fn mutate(&self, asset: &mut A);177}178179impl<M: MutateAsset<A>, A: Asset> RootAssetTransformer<M, A> {180fn new(m: M) -> Self {181Self(m, PhantomData)182}183}184185impl<M: MutateAsset<A>, A: Asset> AssetTransformer for RootAssetTransformer<M, A> {186type AssetInput = A;187type AssetOutput = A;188type Error = std::io::Error;189type Settings = ();190191async fn transform<'a>(192&'a self,193mut asset: TransformedAsset<A>,194_settings: &'a Self::Settings,195) -> Result<TransformedAsset<A>, Self::Error> {196self.0.mutate(asset.get_mut());197Ok(asset)198}199}200201#[test]202fn no_meta_or_default_processor_copies_asset() {203// Assets without a meta file or a default processor should still be accessible in the204// processed path. Note: This isn't exactly the desired property - we don't want the assets205// to be copied to the processed directory. We just want these assets to still be loadable206// if we no longer have the source directory. This could be done with a symlink instead of a207// copy.208209let AppWithProcessor {210mut app,211source_dir,212processed_dir,213} = create_app_with_asset_processor();214215let path = Path::new("abc.cool.ron");216let source_asset = r#"(217text: "abc",218dependencies: [],219embedded_dependencies: [],220sub_texts: [],221)"#;222223source_dir.insert_asset_text(path, source_asset);224225run_app_until_finished_processing(&mut app);226227let processed_asset = processed_dir.get_asset(path).unwrap();228let processed_asset = str::from_utf8(processed_asset.value()).unwrap();229assert_eq!(processed_asset, source_asset);230}231232#[test]233fn asset_processor_transforms_asset_default_processor() {234let AppWithProcessor {235mut app,236source_dir,237processed_dir,238} = create_app_with_asset_processor();239240struct AddText;241242impl MutateAsset<CoolText> for AddText {243fn mutate(&self, text: &mut CoolText) {244text.text.push_str("_def");245}246}247248type CoolTextProcessor = LoadTransformAndSave<249CoolTextLoader,250RootAssetTransformer<AddText, CoolText>,251CoolTextSaver,252>;253app.register_asset_loader(CoolTextLoader)254.register_asset_processor(CoolTextProcessor::new(255RootAssetTransformer::new(AddText),256CoolTextSaver,257))258.set_default_asset_processor::<CoolTextProcessor>("cool.ron");259260let path = Path::new("abc.cool.ron");261source_dir.insert_asset_text(262path,263r#"(264text: "abc",265dependencies: [],266embedded_dependencies: [],267sub_texts: [],268)"#,269);270271run_app_until_finished_processing(&mut app);272273let processed_asset = processed_dir.get_asset(path).unwrap();274let processed_asset = str::from_utf8(processed_asset.value()).unwrap();275assert_eq!(276processed_asset,277r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#278);279}280281#[test]282fn asset_processor_transforms_asset_with_meta() {283let AppWithProcessor {284mut app,285source_dir,286processed_dir,287} = create_app_with_asset_processor();288289struct AddText;290291impl MutateAsset<CoolText> for AddText {292fn mutate(&self, text: &mut CoolText) {293text.text.push_str("_def");294}295}296297type CoolTextProcessor = LoadTransformAndSave<298CoolTextLoader,299RootAssetTransformer<AddText, CoolText>,300CoolTextSaver,301>;302app.register_asset_loader(CoolTextLoader)303.register_asset_processor(CoolTextProcessor::new(304RootAssetTransformer::new(AddText),305CoolTextSaver,306));307308let path = Path::new("abc.cool.ron");309source_dir.insert_asset_text(310path,311r#"(312text: "abc",313dependencies: [],314embedded_dependencies: [],315sub_texts: [],316)"#,317);318source_dir.insert_meta_text(path, r#"(319meta_format_version: "1.0",320asset: Process(321processor: "bevy_asset::processor::process::LoadTransformAndSave<bevy_asset::tests::CoolTextLoader, bevy_asset::processor::tests::RootAssetTransformer<bevy_asset::processor::tests::asset_processor_transforms_asset_with_meta::AddText, bevy_asset::tests::CoolText>, bevy_asset::processor::tests::CoolTextSaver>",322settings: (323loader_settings: (),324transformer_settings: (),325saver_settings: (),326),327),328)"#);329330run_app_until_finished_processing(&mut app);331332let processed_asset = processed_dir.get_asset(path).unwrap();333let processed_asset = str::from_utf8(processed_asset.value()).unwrap();334assert_eq!(335processed_asset,336r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#337);338}339340#[derive(Asset, TypePath, Serialize, Deserialize)]341struct FakeGltf {342gltf_nodes: BTreeMap<String, String>,343}344345struct FakeGltfLoader;346347impl AssetLoader for FakeGltfLoader {348type Asset = FakeGltf;349type Settings = ();350type Error = std::io::Error;351352async fn load(353&self,354reader: &mut dyn Reader,355_settings: &Self::Settings,356_load_context: &mut LoadContext<'_>,357) -> Result<Self::Asset, Self::Error> {358use std::io::{Error, ErrorKind};359360let mut bytes = vec![];361reader.read_to_end(&mut bytes).await?;362ron::de::from_bytes(&bytes)363.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))364}365366fn extensions(&self) -> &[&str] {367&["gltf"]368}369}370371#[derive(Asset, TypePath, Serialize, Deserialize)]372struct FakeBsn {373parent_bsn: Option<String>,374nodes: BTreeMap<String, String>,375}376377// This loader loads the BSN but as an "inlined" scene. We read the original BSN and create a378// scene that holds all the data including parents.379// TODO: It would be nice if the inlining was actually done as an `AssetTransformer`, but380// `Process` currently has no way to load nested assets.381struct FakeBsnLoader;382383impl AssetLoader for FakeBsnLoader {384type Asset = FakeBsn;385type Settings = ();386type Error = std::io::Error;387388async fn load(389&self,390reader: &mut dyn Reader,391_settings: &Self::Settings,392load_context: &mut LoadContext<'_>,393) -> Result<Self::Asset, Self::Error> {394use std::io::{Error, ErrorKind};395396let mut bytes = vec![];397reader.read_to_end(&mut bytes).await?;398let bsn: FakeBsn = ron::de::from_bytes(&bytes)399.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;400401if bsn.parent_bsn.is_none() {402return Ok(bsn);403}404405let parent_bsn = bsn.parent_bsn.unwrap();406let parent_bsn = load_context407.loader()408.immediate()409.load(parent_bsn)410.await411.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;412let mut new_bsn: FakeBsn = parent_bsn.take();413for (name, node) in bsn.nodes {414new_bsn.nodes.insert(name, node);415}416Ok(new_bsn)417}418419fn extensions(&self) -> &[&str] {420&["bsn"]421}422}423424#[derive(TypePath)]425struct GltfToBsn;426427impl AssetTransformer for GltfToBsn {428type AssetInput = FakeGltf;429type AssetOutput = FakeBsn;430type Settings = ();431type Error = std::io::Error;432433async fn transform<'a>(434&'a self,435mut asset: TransformedAsset<Self::AssetInput>,436_settings: &'a Self::Settings,437) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {438let bsn = FakeBsn {439parent_bsn: None,440// Pretend we converted all the glTF nodes into BSN's format.441nodes: core::mem::take(&mut asset.get_mut().gltf_nodes),442};443Ok(asset.replace_asset(bsn))444}445}446447#[derive(TypePath)]448struct FakeBsnSaver;449450impl AssetSaver for FakeBsnSaver {451type Asset = FakeBsn;452type Error = std::io::Error;453type OutputLoader = FakeBsnLoader;454type Settings = ();455456async fn save(457&self,458writer: &mut crate::io::Writer,459asset: crate::saver::SavedAsset<'_, Self::Asset>,460_settings: &Self::Settings,461) -> Result<(), Self::Error> {462use std::io::{Error, ErrorKind};463464use ron::ser::PrettyConfig;465466let ron_string =467ron::ser::to_string_pretty(asset.get(), PrettyConfig::new().new_line("\n"))468.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;469470writer.write_all(ron_string.as_bytes()).await471}472}473#[test]474fn asset_processor_loading_can_read_processed_assets() {475use crate::transformer::IdentityAssetTransformer;476477let AppWithProcessor {478mut app,479source_dir,480processed_dir,481} = create_app_with_asset_processor();482483// This processor loads a gltf file, converts it to BSN and then saves out the BSN.484type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;485// This processor loads a BSN file (which "inlines" parent BSNs at load), and then saves the486// inlined BSN.487type BsnProcessor =488LoadTransformAndSave<FakeBsnLoader, IdentityAssetTransformer<FakeBsn>, FakeBsnSaver>;489app.register_asset_loader(FakeBsnLoader)490.register_asset_loader(FakeGltfLoader)491.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))492.register_asset_processor(BsnProcessor::new(493IdentityAssetTransformer::new(),494FakeBsnSaver,495))496.set_default_asset_processor::<GltfProcessor>("gltf")497.set_default_asset_processor::<BsnProcessor>("bsn");498499let gltf_path = Path::new("abc.gltf");500source_dir.insert_asset_text(501gltf_path,502r#"(503gltf_nodes: {504"name": "thing",505"position": "123",506}507)"#,508);509let bsn_path = Path::new("def.bsn");510// The bsn tries to load the gltf as a bsn. This only works if the bsn can read processed511// assets.512source_dir.insert_asset_text(513bsn_path,514r#"(515parent_bsn: Some("abc.gltf"),516nodes: {517"position": "456",518"color": "red",519},520)"#,521);522523run_app_until_finished_processing(&mut app);524525let processed_bsn = processed_dir.get_asset(bsn_path).unwrap();526let processed_bsn = str::from_utf8(processed_bsn.value()).unwrap();527// The processed bsn should have been "inlined", so no parent and "overlaid" nodes.528assert_eq!(529processed_bsn,530r#"(531parent_bsn: None,532nodes: {533"color": "red",534"name": "thing",535"position": "456",536},537)"#538);539}540541#[test]542fn asset_processor_loading_can_read_source_assets() {543let AppWithProcessor {544mut app,545source_dir,546processed_dir,547} = create_app_with_asset_processor();548549#[derive(Serialize, Deserialize)]550struct FakeGltfxData {551// These are the file paths to the gltfs.552gltfs: Vec<String>,553}554555#[derive(Asset, TypePath)]556struct FakeGltfx {557gltfs: Vec<FakeGltf>,558}559560#[derive(TypePath)]561struct FakeGltfxLoader;562563impl AssetLoader for FakeGltfxLoader {564type Asset = FakeGltfx;565type Error = std::io::Error;566type Settings = ();567568async fn load(569&self,570reader: &mut dyn Reader,571_settings: &Self::Settings,572load_context: &mut LoadContext<'_>,573) -> Result<Self::Asset, Self::Error> {574use std::io::{Error, ErrorKind};575576let mut buf = vec![];577reader.read_to_end(&mut buf).await?;578579let gltfx_data: FakeGltfxData =580ron::de::from_bytes(&buf).map_err(|err| Error::new(ErrorKind::InvalidData, err))?;581582let mut gltfs = vec![];583for gltf in gltfx_data.gltfs.into_iter() {584// gltfx files come from "generic" software that doesn't know anything about585// Bevy, so it needs to load the source assets to make sense.586let gltf = load_context587.loader()588.immediate()589.load(gltf)590.await591.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;592gltfs.push(gltf.take());593}594595Ok(FakeGltfx { gltfs })596}597598fn extensions(&self) -> &[&str] {599&["gltfx"]600}601}602603#[derive(TypePath)]604struct GltfxToBsn;605606impl AssetTransformer for GltfxToBsn {607type AssetInput = FakeGltfx;608type AssetOutput = FakeBsn;609type Settings = ();610type Error = std::io::Error;611612async fn transform<'a>(613&'a self,614mut asset: TransformedAsset<Self::AssetInput>,615_settings: &'a Self::Settings,616) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {617let gltfx = asset.get_mut();618619// Merge together all the gltfs from the gltfx into one big bsn.620let bsn = gltfx.gltfs.drain(..).fold(621FakeBsn {622parent_bsn: None,623nodes: Default::default(),624},625|mut bsn, gltf| {626for (key, value) in gltf.gltf_nodes {627bsn.nodes.insert(key, value);628}629bsn630},631);632633Ok(asset.replace_asset(bsn))634}635}636637// This processor loads a gltf file, converts it to BSN and then saves out the BSN.638type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;639// This processor loads a gltfx file (including its gltf files) and converts it to BSN.640type GltfxProcessor = LoadTransformAndSave<FakeGltfxLoader, GltfxToBsn, FakeBsnSaver>;641app.register_asset_loader(FakeGltfLoader)642.register_asset_loader(FakeGltfxLoader)643.register_asset_loader(FakeBsnLoader)644.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))645.register_asset_processor(GltfxProcessor::new(GltfxToBsn, FakeBsnSaver))646.set_default_asset_processor::<GltfProcessor>("gltf")647.set_default_asset_processor::<GltfxProcessor>("gltfx");648649let gltf_path_1 = Path::new("abc.gltf");650source_dir.insert_asset_text(651gltf_path_1,652r#"(653gltf_nodes: {654"name": "thing",655"position": "123",656}657)"#,658);659let gltf_path_2 = Path::new("def.gltf");660source_dir.insert_asset_text(661gltf_path_2,662r#"(663gltf_nodes: {664"velocity": "456",665"color": "red",666}667)"#,668);669670let gltfx_path = Path::new("xyz.gltfx");671source_dir.insert_asset_text(672gltfx_path,673r#"(674gltfs: ["abc.gltf", "def.gltf"],675)"#,676);677678run_app_until_finished_processing(&mut app);679680// Sanity check that the two gltf files were actually processed.681let processed_gltf_1 = processed_dir.get_asset(gltf_path_1).unwrap();682let processed_gltf_1 = str::from_utf8(processed_gltf_1.value()).unwrap();683assert_eq!(684processed_gltf_1,685r#"(686parent_bsn: None,687nodes: {688"name": "thing",689"position": "123",690},691)"#692);693let processed_gltf_2 = processed_dir.get_asset(gltf_path_2).unwrap();694let processed_gltf_2 = str::from_utf8(processed_gltf_2.value()).unwrap();695assert_eq!(696processed_gltf_2,697r#"(698parent_bsn: None,699nodes: {700"color": "red",701"velocity": "456",702},703)"#704);705706// The processed gltfx should have been able to load and merge the gltfs despite them having707// been processed into bsn.708709// Blocked on https://github.com/bevyengine/bevy/issues/21269. This is the actual assertion.710// let processed_gltfx = processed_dir.get_asset(gltfx_path).unwrap();711// let processed_gltfx = str::from_utf8(processed_gltfx.value()).unwrap();712// assert_eq!(713// processed_gltfx,714// r#"(715// parent_bsn: None,716// nodes: {717// "color": "red",718// "name": "thing",719// "position": "123",720// "velocity": "456",721// },722// )"#723// );724725// This assertion exists to "prove" that this problem exists.726assert!(processed_dir.get_asset(gltfx_path).is_none());727}728729730