Path: blob/main/crates/bevy_asset/src/processor/tests.rs
9332 views
use alloc::{1boxed::Box,2collections::BTreeMap,3string::{String, ToString},4sync::Arc,5vec,6vec::Vec,7};8use async_lock::{RwLock, RwLockWriteGuard};9use bevy_platform::{10collections::HashMap,11sync::{Mutex, PoisonError},12};13use bevy_reflect::TypePath;14use core::marker::PhantomData;15use futures_lite::AsyncWriteExt;16use ron::ser::PrettyConfig;17use serde::{Deserialize, Serialize};18use std::path::Path;1920use bevy_app::{App, TaskPoolPlugin};21use bevy_ecs::error::BevyError;22use bevy_tasks::BoxedFuture;2324use crate::{25io::{26memory::{Dir, MemoryAssetReader, MemoryAssetWriter},27AssetReader, AssetReaderError, AssetSourceBuilder, AssetSourceBuilders, AssetSourceEvent,28AssetSourceId, AssetWatcher, PathStream, Reader,29},30processor::{31AssetProcessor, GetProcessorError, LoadTransformAndSave, LogEntry, Process, ProcessContext,32ProcessError, ProcessorState, ProcessorTransactionLog, ProcessorTransactionLogFactory,33},34saver::{tests::CoolTextSaver, AssetSaver},35tests::{36read_asset_as_string, read_meta_as_string, run_app_until, CoolText, CoolTextLoader,37CoolTextRon, SubText,38},39transformer::{AssetTransformer, TransformedAsset},40Asset, AssetApp, AssetLoader, AssetMode, AssetPath, AssetPlugin, LoadContext,41WriteDefaultMetaError,42};4344#[derive(TypePath)]45struct MyProcessor<T>(PhantomData<fn() -> T>);4647impl<T: TypePath + 'static> Process for MyProcessor<T> {48type OutputLoader = ();49type Settings = ();5051async fn process(52&self,53_context: &mut ProcessContext<'_>,54_settings: &Self::Settings,55_writer: &mut crate::io::Writer,56) -> Result<(), ProcessError> {57Ok(())58}59}6061#[derive(TypePath)]62struct Marker;6364fn create_empty_asset_processor() -> AssetProcessor {65let mut sources = AssetSourceBuilders::default();66// Create an empty asset source so that AssetProcessor is happy.67let dir = Dir::default();68let memory_reader = MemoryAssetReader { root: dir.clone() };69sources.insert(70AssetSourceId::Default,71AssetSourceBuilder::new(move || Box::new(memory_reader.clone())),72);7374AssetProcessor::new(&mut sources, false).075}7677#[test]78fn get_asset_processor_by_name() {79let asset_processor = create_empty_asset_processor();80asset_processor.register_processor(MyProcessor::<Marker>(PhantomData));8182let long_processor = asset_processor83.get_processor(84"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",85)86.expect("Processor was previously registered");87let short_processor = asset_processor88.get_processor("MyProcessor<Marker>")89.expect("Processor was previously registered");9091// We can use either the long or short processor name and we will get the same processor92// out.93assert!(Arc::ptr_eq(&long_processor, &short_processor));94}9596#[test]97fn missing_processor_returns_error() {98let asset_processor = create_empty_asset_processor();99100let Err(long_processor_err) = asset_processor.get_processor(101"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",102) else {103panic!("Processor was returned even though we never registered any.");104};105let GetProcessorError::Missing(long_processor_err) = &long_processor_err else {106panic!("get_processor returned incorrect error: {long_processor_err}");107};108assert_eq!(109long_processor_err,110"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>"111);112113// Short paths should also return an error.114115let Err(long_processor_err) = asset_processor.get_processor("MyProcessor<Marker>") else {116panic!("Processor was returned even though we never registered any.");117};118let GetProcessorError::Missing(long_processor_err) = &long_processor_err else {119panic!("get_processor returned incorrect error: {long_processor_err}");120};121assert_eq!(long_processor_err, "MyProcessor<Marker>");122}123124// Create another marker type whose short name will overlap `Marker`.125mod sneaky {126use bevy_reflect::TypePath;127128#[derive(TypePath)]129pub struct Marker;130}131132#[test]133fn ambiguous_short_path_returns_error() {134let asset_processor = create_empty_asset_processor();135asset_processor.register_processor(MyProcessor::<Marker>(PhantomData));136asset_processor.register_processor(MyProcessor::<sneaky::Marker>(PhantomData));137138let Err(long_processor_err) = asset_processor.get_processor("MyProcessor<Marker>") else {139panic!("Processor was returned even though the short path is ambiguous.");140};141let GetProcessorError::Ambiguous {142processor_short_name,143ambiguous_processor_names,144} = &long_processor_err145else {146panic!("get_processor returned incorrect error: {long_processor_err}");147};148assert_eq!(processor_short_name, "MyProcessor<Marker>");149let expected_ambiguous_names = [150"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",151"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::sneaky::Marker>",152];153assert_eq!(ambiguous_processor_names, &expected_ambiguous_names);154155let processor_1 = asset_processor156.get_processor(157"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",158)159.expect("Processor was previously registered");160let processor_2 = asset_processor161.get_processor(162"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::sneaky::Marker>",163)164.expect("Processor was previously registered");165166// If we fully specify the paths, we get the two different processors.167assert!(!Arc::ptr_eq(&processor_1, &processor_2));168}169170#[derive(Clone)]171struct ProcessingDirs {172source: Dir,173processed: Dir,174source_event_sender: async_channel::Sender<AssetSourceEvent>,175}176177struct AppWithProcessor {178app: App,179source_gate: Arc<RwLock<()>>,180default_source_dirs: ProcessingDirs,181extra_sources_dirs: HashMap<String, ProcessingDirs>,182}183184/// Similar to [`crate::io::gated::GatedReader`], but uses a lock instead of a channel to avoid185/// needing to send the "correct" number of messages.186#[derive(Clone)]187struct LockGatedReader<R: AssetReader> {188reader: R,189gate: Arc<RwLock<()>>,190}191192impl<R: AssetReader> LockGatedReader<R> {193/// Creates a new [`GatedReader`], which wraps the given `reader`. Also returns a [`GateOpener`] which194/// can be used to open "path gates" for this [`GatedReader`].195fn new(gate: Arc<RwLock<()>>, reader: R) -> Self {196Self { gate, reader }197}198}199200impl<R: AssetReader> AssetReader for LockGatedReader<R> {201async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {202let _guard = self.gate.read().await;203self.reader.read(path).await204}205206async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {207let _guard = self.gate.read().await;208self.reader.read_meta(path).await209}210211async fn read_directory<'a>(212&'a self,213path: &'a Path,214) -> Result<Box<PathStream>, AssetReaderError> {215let _guard = self.gate.read().await;216self.reader.read_directory(path).await217}218219async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {220let _guard = self.gate.read().await;221self.reader.is_directory(path).await222}223}224225/// Serializes `text` into a `CoolText` that can be loaded.226///227/// This doesn't support all the features of `CoolText`, so more complex scenarios may require doing228/// this manually.229fn serialize_as_cool_text(text: &str) -> String {230let cool_text_ron = CoolTextRon {231text: text.into(),232dependencies: vec![],233embedded_dependencies: vec![],234sub_texts: vec![],235};236ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()237}238239/// Sets the transaction log for the app to a fake one to prevent touching the filesystem.240fn set_fake_transaction_log(app: &mut App) {241/// A dummy transaction log factory that just creates [`FakeTransactionLog`].242struct FakeTransactionLogFactory;243244impl ProcessorTransactionLogFactory for FakeTransactionLogFactory {245fn read(&self) -> BoxedFuture<'_, Result<Vec<LogEntry>, BevyError>> {246Box::pin(async move { Ok(vec![]) })247}248249fn create_new_log(250&self,251) -> BoxedFuture<'_, Result<Box<dyn ProcessorTransactionLog>, BevyError>> {252Box::pin(async move { Ok(Box::new(FakeTransactionLog) as _) })253}254}255256/// A dummy transaction log that just drops every log.257// TODO: In the future it's possible for us to have a test of the transaction log, so making258// this more complex may be necessary.259struct FakeTransactionLog;260261impl ProcessorTransactionLog for FakeTransactionLog {262fn begin_processing<'a>(263&'a mut self,264_asset: &'a AssetPath<'_>,265) -> BoxedFuture<'a, Result<(), BevyError>> {266Box::pin(async move { Ok(()) })267}268269fn end_processing<'a>(270&'a mut self,271_asset: &'a AssetPath<'_>,272) -> BoxedFuture<'a, Result<(), BevyError>> {273Box::pin(async move { Ok(()) })274}275276fn unrecoverable(&mut self) -> BoxedFuture<'_, Result<(), BevyError>> {277Box::pin(async move { Ok(()) })278}279}280281app.world()282.resource::<AssetProcessor>()283.data()284.set_log_factory(Box::new(FakeTransactionLogFactory))285.unwrap();286}287288fn create_app_with_asset_processor(extra_sources: &[String]) -> AppWithProcessor {289let mut app = App::new();290let source_gate = Arc::new(RwLock::new(()));291292struct UnfinishedProcessingDirs {293source: Dir,294processed: Dir,295// The receiver channel for the source event sender for the unprocessed source.296source_event_sender_receiver:297async_channel::Receiver<async_channel::Sender<AssetSourceEvent>>,298}299300impl UnfinishedProcessingDirs {301fn finish(self) -> ProcessingDirs {302ProcessingDirs {303source: self.source,304processed: self.processed,305// The processor listens for events on the source unconditionally, and we enable306// watching for the processed source, so both of these channels will be filled.307source_event_sender: self.source_event_sender_receiver.recv_blocking().unwrap(),308}309}310}311312fn create_source(313app: &mut App,314source_id: AssetSourceId<'static>,315source_gate: Arc<RwLock<()>>,316) -> UnfinishedProcessingDirs {317let source_dir = Dir::default();318let processed_dir = Dir::default();319320let source_memory_reader = LockGatedReader::new(321source_gate,322MemoryAssetReader {323root: source_dir.clone(),324},325);326let source_memory_writer = MemoryAssetWriter {327root: source_dir.clone(),328};329let processed_memory_reader = MemoryAssetReader {330root: processed_dir.clone(),331};332let processed_memory_writer = MemoryAssetWriter {333root: processed_dir.clone(),334};335336let (source_event_sender_sender, source_event_sender_receiver) = async_channel::bounded(1);337338struct FakeWatcher;339340impl AssetWatcher for FakeWatcher {}341342app.register_asset_source(343source_id,344AssetSourceBuilder::new(move || Box::new(source_memory_reader.clone()))345.with_writer(move |_| Some(Box::new(source_memory_writer.clone())))346.with_watcher(move |sender: async_channel::Sender<AssetSourceEvent>| {347source_event_sender_sender.send_blocking(sender).unwrap();348Some(Box::new(FakeWatcher))349})350.with_processed_reader(move || Box::new(processed_memory_reader.clone()))351.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),352);353354UnfinishedProcessingDirs {355source: source_dir,356processed: processed_dir,357source_event_sender_receiver,358}359}360361let default_source_dirs = create_source(&mut app, AssetSourceId::Default, source_gate.clone());362363let extra_sources_dirs = extra_sources364.iter()365.map(|source_name| {366(367source_name.clone(),368create_source(369&mut app,370AssetSourceId::Name(source_name.clone().into()),371source_gate.clone(),372),373)374})375.collect::<Vec<_>>();376377app.add_plugins((378TaskPoolPlugin::default(),379AssetPlugin {380mode: AssetMode::Processed,381use_asset_processor_override: Some(true),382watch_for_changes_override: Some(true),383..Default::default()384},385));386387set_fake_transaction_log(&mut app);388389// Now that we've built the app, finish all the processing dirs.390391AppWithProcessor {392app,393source_gate,394default_source_dirs: default_source_dirs.finish(),395extra_sources_dirs: extra_sources_dirs396.into_iter()397.map(|(name, dirs)| (name, dirs.finish()))398.collect(),399}400}401402fn run_app_until_finished_processing(app: &mut App, guard: RwLockWriteGuard<'_, ()>) {403let processor = app.world().resource::<AssetProcessor>().clone();404// We can't just wait for the processor state to be finished since we could have already405// finished before, but now that something has changed, we may not have restarted processing406// yet. So wait for processing to start, then finish.407run_app_until(app, |_| {408// Before we even consider whether the processor is started, make sure that none of the409// receivers have anything left in them. This prevents us accidentally, considering the410// processor as processing before all the events have been processed.411for source in processor.sources().iter() {412let Some(recv) = source.event_receiver() else {413continue;414};415if !recv.is_empty() {416return None;417}418}419let state = bevy_tasks::block_on(processor.get_state());420(state == ProcessorState::Processing || state == ProcessorState::Initializing).then_some(())421});422drop(guard);423run_app_until(app, |_| {424(bevy_tasks::block_on(processor.get_state()) == ProcessorState::Finished).then_some(())425});426}427428// Note: while we allow any Fn, since closures are unnameable types, creating a processor with a429// closure cannot be used (since we need to include the name of the transformer in the meta430// file).431#[derive(TypePath)]432struct RootAssetTransformer<M: MutateAsset<A>, A: Asset>(M, PhantomData<fn(&mut A)>);433434trait MutateAsset<A: Asset>: TypePath + Send + Sync + 'static {435fn mutate(&self, asset: &mut A);436}437438impl<M: MutateAsset<A>, A: Asset> RootAssetTransformer<M, A> {439fn new(m: M) -> Self {440Self(m, PhantomData)441}442}443444impl<M: MutateAsset<A>, A: Asset> AssetTransformer for RootAssetTransformer<M, A> {445type AssetInput = A;446type AssetOutput = A;447type Error = std::io::Error;448type Settings = ();449450async fn transform<'a>(451&'a self,452mut asset: TransformedAsset<A>,453_settings: &'a Self::Settings,454) -> Result<TransformedAsset<A>, Self::Error> {455self.0.mutate(asset.get_mut());456Ok(asset)457}458}459460#[derive(TypePath)]461struct AddText(String);462463impl MutateAsset<CoolText> for AddText {464fn mutate(&self, text: &mut CoolText) {465text.text.push_str(&self.0);466}467}468469#[test]470fn no_meta_or_default_processor_copies_asset() {471// Assets without a meta file or a default processor should still be accessible in the472// processed path. Note: This isn't exactly the desired property - we don't want the assets473// to be copied to the processed directory. We just want these assets to still be loadable474// if we no longer have the source directory. This could be done with a symlink instead of a475// copy.476477let AppWithProcessor {478mut app,479source_gate,480default_source_dirs:481ProcessingDirs {482source: source_dir,483processed: processed_dir,484..485},486..487} = create_app_with_asset_processor(&[]);488489let guard = source_gate.write_blocking();490491let path = Path::new("abc.cool.ron");492let source_asset = r#"(493text: "abc",494dependencies: [],495embedded_dependencies: [],496sub_texts: [],497)"#;498499source_dir.insert_asset_text(path, source_asset);500501run_app_until_finished_processing(&mut app, guard);502503let processed_asset = processed_dir.get_asset(path).unwrap();504let processed_asset = str::from_utf8(processed_asset.value()).unwrap();505assert_eq!(processed_asset, source_asset);506}507508#[test]509fn asset_processor_transforms_asset_default_processor() {510let AppWithProcessor {511mut app,512source_gate,513default_source_dirs:514ProcessingDirs {515source: source_dir,516processed: processed_dir,517..518},519..520} = create_app_with_asset_processor(&[]);521522type CoolTextProcessor = LoadTransformAndSave<523CoolTextLoader,524RootAssetTransformer<AddText, CoolText>,525CoolTextSaver,526>;527app.register_asset_loader(CoolTextLoader)528.register_asset_processor(CoolTextProcessor::new(529RootAssetTransformer::new(AddText("_def".into())),530CoolTextSaver,531))532.set_default_asset_processor::<CoolTextProcessor>("cool.ron");533534let guard = source_gate.write_blocking();535536let path = Path::new("abc.cool.ron");537source_dir.insert_asset_text(538path,539r#"(540text: "abc",541dependencies: [],542embedded_dependencies: [],543sub_texts: [],544)"#,545);546547run_app_until_finished_processing(&mut app, guard);548549let processed_asset = processed_dir.get_asset(path).unwrap();550let processed_asset = str::from_utf8(processed_asset.value()).unwrap();551assert_eq!(552processed_asset,553r#"(554text: "abc_def",555dependencies: [],556embedded_dependencies: [],557sub_texts: [],558)"#559);560}561562#[test]563fn asset_processor_transforms_asset_with_meta() {564let AppWithProcessor {565mut app,566source_gate,567default_source_dirs:568ProcessingDirs {569source: source_dir,570processed: processed_dir,571..572},573..574} = create_app_with_asset_processor(&[]);575576type CoolTextProcessor = LoadTransformAndSave<577CoolTextLoader,578RootAssetTransformer<AddText, CoolText>,579CoolTextSaver,580>;581app.register_asset_loader(CoolTextLoader)582.register_asset_processor(CoolTextProcessor::new(583RootAssetTransformer::new(AddText("_def".into())),584CoolTextSaver,585));586587let guard = source_gate.write_blocking();588589let path = Path::new("abc.cool.ron");590source_dir.insert_asset_text(591path,592r#"(593text: "abc",594dependencies: [],595embedded_dependencies: [],596sub_texts: [],597)"#,598);599source_dir.insert_meta_text(path, r#"(600meta_format_version: "1.0",601asset: Process(602processor: "bevy_asset::processor::process::LoadTransformAndSave<bevy_asset::tests::CoolTextLoader, bevy_asset::processor::tests::RootAssetTransformer<bevy_asset::processor::tests::AddText, bevy_asset::tests::CoolText>, bevy_asset::saver::tests::CoolTextSaver>",603settings: (604loader_settings: (),605transformer_settings: (),606saver_settings: (),607),608),609)"#);610611run_app_until_finished_processing(&mut app, guard);612613let processed_asset = processed_dir.get_asset(path).unwrap();614let processed_asset = str::from_utf8(processed_asset.value()).unwrap();615assert_eq!(616processed_asset,617r#"(618text: "abc_def",619dependencies: [],620embedded_dependencies: [],621sub_texts: [],622)"#623);624}625626#[test]627fn asset_processor_transforms_asset_with_short_path_meta() {628let AppWithProcessor {629mut app,630source_gate,631default_source_dirs:632ProcessingDirs {633source: source_dir,634processed: processed_dir,635..636},637..638} = create_app_with_asset_processor(&[]);639640type CoolTextProcessor = LoadTransformAndSave<641CoolTextLoader,642RootAssetTransformer<AddText, CoolText>,643CoolTextSaver,644>;645app.register_asset_loader(CoolTextLoader)646.register_asset_processor(CoolTextProcessor::new(647RootAssetTransformer::new(AddText("_def".into())),648CoolTextSaver,649));650651let guard = source_gate.write_blocking();652653let path = Path::new("abc.cool.ron");654source_dir.insert_asset_text(655path,656r#"(657text: "abc",658dependencies: [],659embedded_dependencies: [],660sub_texts: [],661)"#,662);663source_dir.insert_meta_text(path, r#"(664meta_format_version: "1.0",665asset: Process(666processor: "LoadTransformAndSave<CoolTextLoader, RootAssetTransformer<AddText, CoolText>, CoolTextSaver>",667settings: (668loader_settings: (),669transformer_settings: (),670saver_settings: (),671),672),673)"#);674675run_app_until_finished_processing(&mut app, guard);676677let processed_asset = processed_dir.get_asset(path).unwrap();678let processed_asset = str::from_utf8(processed_asset.value()).unwrap();679assert_eq!(680processed_asset,681r#"(682text: "abc_def",683dependencies: [],684embedded_dependencies: [],685sub_texts: [],686)"#687);688}689690#[derive(Asset, TypePath, Serialize, Deserialize)]691struct FakeGltf {692gltf_nodes: BTreeMap<String, String>,693}694695#[derive(TypePath)]696struct FakeGltfLoader;697698impl AssetLoader for FakeGltfLoader {699type Asset = FakeGltf;700type Settings = ();701type Error = std::io::Error;702703async fn load(704&self,705reader: &mut dyn Reader,706_settings: &Self::Settings,707_load_context: &mut LoadContext<'_>,708) -> Result<Self::Asset, Self::Error> {709use std::io::{Error, ErrorKind};710711let mut bytes = vec![];712reader.read_to_end(&mut bytes).await?;713ron::de::from_bytes(&bytes)714.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))715}716717fn extensions(&self) -> &[&str] {718&["gltf"]719}720}721722#[derive(Asset, TypePath, Serialize, Deserialize)]723struct FakeBsn {724parent_bsn: Option<String>,725nodes: BTreeMap<String, String>,726}727728// This loader loads the BSN but as an "inlined" scene. We read the original BSN and create a729// scene that holds all the data including parents.730// TODO: It would be nice if the inlining was actually done as an `AssetTransformer`, but731// `Process` currently has no way to load nested assets.732#[derive(TypePath)]733struct FakeBsnLoader;734735impl AssetLoader for FakeBsnLoader {736type Asset = FakeBsn;737type Settings = ();738type Error = std::io::Error;739740async fn load(741&self,742reader: &mut dyn Reader,743_settings: &Self::Settings,744load_context: &mut LoadContext<'_>,745) -> Result<Self::Asset, Self::Error> {746use std::io::{Error, ErrorKind};747748let mut bytes = vec![];749reader.read_to_end(&mut bytes).await?;750let bsn: FakeBsn = ron::de::from_bytes(&bytes)751.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;752753if bsn.parent_bsn.is_none() {754return Ok(bsn);755}756757let parent_bsn = bsn.parent_bsn.unwrap();758let parent_bsn = load_context759.loader()760.immediate()761.load(parent_bsn)762.await763.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;764let mut new_bsn: FakeBsn = parent_bsn.take();765for (name, node) in bsn.nodes {766new_bsn.nodes.insert(name, node);767}768Ok(new_bsn)769}770771fn extensions(&self) -> &[&str] {772&["bsn"]773}774}775776#[derive(TypePath)]777struct GltfToBsn;778779impl AssetTransformer for GltfToBsn {780type AssetInput = FakeGltf;781type AssetOutput = FakeBsn;782type Settings = ();783type Error = std::io::Error;784785async fn transform<'a>(786&'a self,787mut asset: TransformedAsset<Self::AssetInput>,788_settings: &'a Self::Settings,789) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {790let bsn = FakeBsn {791parent_bsn: None,792// Pretend we converted all the glTF nodes into BSN's format.793nodes: core::mem::take(&mut asset.get_mut().gltf_nodes),794};795Ok(asset.replace_asset(bsn))796}797}798799#[derive(TypePath)]800struct FakeBsnSaver;801802impl AssetSaver for FakeBsnSaver {803type Asset = FakeBsn;804type Error = std::io::Error;805type OutputLoader = FakeBsnLoader;806type Settings = ();807808async fn save(809&self,810writer: &mut crate::io::Writer,811asset: crate::saver::SavedAsset<'_, '_, Self::Asset>,812_settings: &Self::Settings,813) -> Result<(), Self::Error> {814use std::io::{Error, ErrorKind};815816let ron_string =817ron::ser::to_string_pretty(asset.get(), PrettyConfig::new().new_line("\n"))818.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;819820writer.write_all(ron_string.as_bytes()).await821}822}823#[test]824fn asset_processor_loading_can_read_processed_assets() {825use crate::transformer::IdentityAssetTransformer;826827let AppWithProcessor {828mut app,829source_gate,830default_source_dirs:831ProcessingDirs {832source: source_dir,833processed: processed_dir,834..835},836..837} = create_app_with_asset_processor(&[]);838839// This processor loads a gltf file, converts it to BSN and then saves out the BSN.840type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;841// This processor loads a BSN file (which "inlines" parent BSNs at load), and then saves the842// inlined BSN.843type BsnProcessor =844LoadTransformAndSave<FakeBsnLoader, IdentityAssetTransformer<FakeBsn>, FakeBsnSaver>;845app.register_asset_loader(FakeBsnLoader)846.register_asset_loader(FakeGltfLoader)847.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))848.register_asset_processor(BsnProcessor::new(849IdentityAssetTransformer::new(),850FakeBsnSaver,851))852.set_default_asset_processor::<GltfProcessor>("gltf")853.set_default_asset_processor::<BsnProcessor>("bsn");854855let guard = source_gate.write_blocking();856857let gltf_path = Path::new("abc.gltf");858source_dir.insert_asset_text(859gltf_path,860r#"(861gltf_nodes: {862"name": "thing",863"position": "123",864}865)"#,866);867let bsn_path = Path::new("def.bsn");868// The bsn tries to load the gltf as a bsn. This only works if the bsn can read processed869// assets.870source_dir.insert_asset_text(871bsn_path,872r#"(873parent_bsn: Some("abc.gltf"),874nodes: {875"position": "456",876"color": "red",877},878)"#,879);880881run_app_until_finished_processing(&mut app, guard);882883let processed_bsn = processed_dir.get_asset(bsn_path).unwrap();884let processed_bsn = str::from_utf8(processed_bsn.value()).unwrap();885// The processed bsn should have been "inlined", so no parent and "overlaid" nodes.886assert_eq!(887processed_bsn,888r#"(889parent_bsn: None,890nodes: {891"color": "red",892"name": "thing",893"position": "456",894},895)"#896);897}898899#[test]900fn asset_processor_loading_can_read_source_assets() {901let AppWithProcessor {902mut app,903source_gate,904default_source_dirs:905ProcessingDirs {906source: source_dir,907processed: processed_dir,908..909},910..911} = create_app_with_asset_processor(&[]);912913#[derive(Serialize, Deserialize)]914struct FakeGltfxData {915// These are the file paths to the gltfs.916gltfs: Vec<String>,917}918919#[derive(Asset, TypePath)]920struct FakeGltfx {921gltfs: Vec<FakeGltf>,922}923924#[derive(TypePath)]925struct FakeGltfxLoader;926927impl AssetLoader for FakeGltfxLoader {928type Asset = FakeGltfx;929type Error = std::io::Error;930type Settings = ();931932async fn load(933&self,934reader: &mut dyn Reader,935_settings: &Self::Settings,936load_context: &mut LoadContext<'_>,937) -> Result<Self::Asset, Self::Error> {938use std::io::{Error, ErrorKind};939940let mut buf = vec![];941reader.read_to_end(&mut buf).await?;942943let gltfx_data: FakeGltfxData =944ron::de::from_bytes(&buf).map_err(|err| Error::new(ErrorKind::InvalidData, err))?;945946let mut gltfs = vec![];947for gltf in gltfx_data.gltfs.into_iter() {948// gltfx files come from "generic" software that doesn't know anything about949// Bevy, so it needs to load the source assets to make sense.950let gltf = load_context951.loader()952.immediate()953.load(gltf)954.await955.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;956gltfs.push(gltf.take());957}958959Ok(FakeGltfx { gltfs })960}961962fn extensions(&self) -> &[&str] {963&["gltfx"]964}965}966967#[derive(TypePath)]968struct GltfxToBsn;969970impl AssetTransformer for GltfxToBsn {971type AssetInput = FakeGltfx;972type AssetOutput = FakeBsn;973type Settings = ();974type Error = std::io::Error;975976async fn transform<'a>(977&'a self,978mut asset: TransformedAsset<Self::AssetInput>,979_settings: &'a Self::Settings,980) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {981let gltfx = asset.get_mut();982983// Merge together all the gltfs from the gltfx into one big bsn.984let bsn = gltfx.gltfs.drain(..).fold(985FakeBsn {986parent_bsn: None,987nodes: Default::default(),988},989|mut bsn, gltf| {990for (key, value) in gltf.gltf_nodes {991bsn.nodes.insert(key, value);992}993bsn994},995);996997Ok(asset.replace_asset(bsn))998}999}10001001// This processor loads a gltf file, converts it to BSN and then saves out the BSN.1002type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;1003// This processor loads a gltfx file (including its gltf files) and converts it to BSN.1004type GltfxProcessor = LoadTransformAndSave<FakeGltfxLoader, GltfxToBsn, FakeBsnSaver>;1005app.register_asset_loader(FakeGltfLoader)1006.register_asset_loader(FakeGltfxLoader)1007.register_asset_loader(FakeBsnLoader)1008.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))1009.register_asset_processor(GltfxProcessor::new(GltfxToBsn, FakeBsnSaver))1010.set_default_asset_processor::<GltfProcessor>("gltf")1011.set_default_asset_processor::<GltfxProcessor>("gltfx");10121013let guard = source_gate.write_blocking();10141015let gltf_path_1 = Path::new("abc.gltf");1016source_dir.insert_asset_text(1017gltf_path_1,1018r#"(1019gltf_nodes: {1020"name": "thing",1021"position": "123",1022}1023)"#,1024);1025let gltf_path_2 = Path::new("def.gltf");1026source_dir.insert_asset_text(1027gltf_path_2,1028r#"(1029gltf_nodes: {1030"velocity": "456",1031"color": "red",1032}1033)"#,1034);10351036let gltfx_path = Path::new("xyz.gltfx");1037source_dir.insert_asset_text(1038gltfx_path,1039r#"(1040gltfs: ["abc.gltf", "def.gltf"],1041)"#,1042);10431044run_app_until_finished_processing(&mut app, guard);10451046// Sanity check that the two gltf files were actually processed.1047let processed_gltf_1 = processed_dir.get_asset(gltf_path_1).unwrap();1048let processed_gltf_1 = str::from_utf8(processed_gltf_1.value()).unwrap();1049assert_eq!(1050processed_gltf_1,1051r#"(1052parent_bsn: None,1053nodes: {1054"name": "thing",1055"position": "123",1056},1057)"#1058);1059let processed_gltf_2 = processed_dir.get_asset(gltf_path_2).unwrap();1060let processed_gltf_2 = str::from_utf8(processed_gltf_2.value()).unwrap();1061assert_eq!(1062processed_gltf_2,1063r#"(1064parent_bsn: None,1065nodes: {1066"color": "red",1067"velocity": "456",1068},1069)"#1070);10711072// The processed gltfx should have been able to load and merge the gltfs despite them having1073// been processed into bsn.10741075// Blocked on https://github.com/bevyengine/bevy/issues/21269. This is the actual assertion.1076// let processed_gltfx = processed_dir.get_asset(gltfx_path).unwrap();1077// let processed_gltfx = str::from_utf8(processed_gltfx.value()).unwrap();1078// assert_eq!(1079// processed_gltfx,1080// r#"(1081// parent_bsn: None,1082// nodes: {1083// "color": "red",1084// "name": "thing",1085// "position": "123",1086// "velocity": "456",1087// },1088// )"#1089// );10901091// This assertion exists to "prove" that this problem exists.1092assert!(processed_dir.get_asset(gltfx_path).is_none());1093}10941095#[test]1096fn asset_processor_processes_all_sources() {1097let AppWithProcessor {1098mut app,1099source_gate,1100default_source_dirs:1101ProcessingDirs {1102source: default_source_dir,1103processed: default_processed_dir,1104source_event_sender: default_source_events,1105},1106extra_sources_dirs,1107} = create_app_with_asset_processor(&["custom_1".into(), "custom_2".into()]);1108let ProcessingDirs {1109source: custom_1_source_dir,1110processed: custom_1_processed_dir,1111source_event_sender: custom_1_source_events,1112} = extra_sources_dirs["custom_1"].clone();1113let ProcessingDirs {1114source: custom_2_source_dir,1115processed: custom_2_processed_dir,1116source_event_sender: custom_2_source_events,1117} = extra_sources_dirs["custom_2"].clone();11181119type AddTextProcessor = LoadTransformAndSave<1120CoolTextLoader,1121RootAssetTransformer<AddText, CoolText>,1122CoolTextSaver,1123>;1124app.init_asset::<CoolText>()1125.init_asset::<SubText>()1126.register_asset_loader(CoolTextLoader)1127.register_asset_processor(AddTextProcessor::new(1128RootAssetTransformer::new(AddText(" processed".into())),1129CoolTextSaver,1130))1131.set_default_asset_processor::<AddTextProcessor>("cool.ron");11321133let guard = source_gate.write_blocking();11341135// All the assets will have the same path, but they will still be separately processed since1136// they are in different sources.1137let path = Path::new("asset.cool.ron");1138default_source_dir.insert_asset_text(path, &serialize_as_cool_text("default asset"));1139custom_1_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 1 asset"));1140custom_2_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 2 asset"));11411142run_app_until_finished_processing(&mut app, guard);11431144// Check that all the assets are processed.1145assert_eq!(1146read_asset_as_string(&default_processed_dir, path),1147serialize_as_cool_text("default asset processed")1148);1149assert_eq!(1150read_asset_as_string(&custom_1_processed_dir, path),1151serialize_as_cool_text("custom 1 asset processed")1152);1153assert_eq!(1154read_asset_as_string(&custom_2_processed_dir, path),1155serialize_as_cool_text("custom 2 asset processed")1156);11571158let guard = source_gate.write_blocking();11591160// Update the default source asset and notify the watcher.1161default_source_dir.insert_asset_text(path, &serialize_as_cool_text("default asset changed"));1162default_source_events1163.send_blocking(AssetSourceEvent::ModifiedAsset(path.to_path_buf()))1164.unwrap();11651166run_app_until_finished_processing(&mut app, guard);11671168// Check that all the assets are processed again.1169assert_eq!(1170read_asset_as_string(&default_processed_dir, path),1171serialize_as_cool_text("default asset changed processed")1172);1173assert_eq!(1174read_asset_as_string(&custom_1_processed_dir, path),1175serialize_as_cool_text("custom 1 asset processed")1176);1177assert_eq!(1178read_asset_as_string(&custom_2_processed_dir, path),1179serialize_as_cool_text("custom 2 asset processed")1180);11811182let guard = source_gate.write_blocking();11831184// Update the custom source assets and notify the watchers.1185custom_1_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 1 asset changed"));1186custom_2_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 2 asset changed"));1187custom_1_source_events1188.send_blocking(AssetSourceEvent::ModifiedAsset(path.to_path_buf()))1189.unwrap();1190custom_2_source_events1191.send_blocking(AssetSourceEvent::ModifiedAsset(path.to_path_buf()))1192.unwrap();11931194run_app_until_finished_processing(&mut app, guard);11951196// Check that all the assets are processed again.1197assert_eq!(1198read_asset_as_string(&default_processed_dir, path),1199serialize_as_cool_text("default asset changed processed")1200);1201assert_eq!(1202read_asset_as_string(&custom_1_processed_dir, path),1203serialize_as_cool_text("custom 1 asset changed processed")1204);1205assert_eq!(1206read_asset_as_string(&custom_2_processed_dir, path),1207serialize_as_cool_text("custom 2 asset changed processed")1208);1209}12101211#[test]1212fn nested_loads_of_processed_asset_reprocesses_on_reload() {1213let AppWithProcessor {1214mut app,1215source_gate,1216default_source_dirs:1217ProcessingDirs {1218source: default_source_dir,1219processed: default_processed_dir,1220source_event_sender: default_source_events,1221},1222extra_sources_dirs,1223} = create_app_with_asset_processor(&["custom".into()]);1224let ProcessingDirs {1225source: custom_source_dir,1226processed: custom_processed_dir,1227source_event_sender: custom_source_events,1228} = extra_sources_dirs["custom"].clone();12291230#[derive(Serialize, Deserialize)]1231enum NesterSerialized {1232Leaf(String),1233Path(String),1234}12351236#[derive(Asset, TypePath)]1237struct Nester {1238value: String,1239}12401241#[derive(TypePath)]1242struct NesterLoader;12431244impl AssetLoader for NesterLoader {1245type Asset = Nester;1246type Settings = ();1247type Error = std::io::Error;12481249async fn load(1250&self,1251reader: &mut dyn Reader,1252_settings: &Self::Settings,1253load_context: &mut LoadContext<'_>,1254) -> Result<Self::Asset, Self::Error> {1255let mut bytes = vec![];1256reader.read_to_end(&mut bytes).await?;12571258let serialized: NesterSerialized = ron::de::from_bytes(&bytes).unwrap();1259Ok(match serialized {1260NesterSerialized::Leaf(value) => Nester { value },1261NesterSerialized::Path(path) => {1262let loaded_asset = load_context.loader().immediate().load(path).await.unwrap();1263loaded_asset.take()1264}1265})1266}12671268fn extensions(&self) -> &[&str] {1269&["nest"]1270}1271}12721273#[derive(TypePath)]1274struct AddTextToNested(String, Arc<Mutex<u32>>);12751276impl MutateAsset<Nester> for AddTextToNested {1277fn mutate(&self, asset: &mut Nester) {1278asset.value.push_str(&self.0);12791280*self.1.lock().unwrap_or_else(PoisonError::into_inner) += 1;1281}1282}12831284fn serialize_as_leaf(value: String) -> String {1285let serialized = NesterSerialized::Leaf(value);1286ron::ser::to_string(&serialized).unwrap()1287}12881289#[derive(TypePath)]1290struct NesterSaver;12911292impl AssetSaver for NesterSaver {1293type Asset = Nester;1294type Error = std::io::Error;1295type Settings = ();1296type OutputLoader = NesterLoader;12971298async fn save(1299&self,1300writer: &mut crate::io::Writer,1301asset: crate::saver::SavedAsset<'_, '_, Self::Asset>,1302_settings: &Self::Settings,1303) -> Result<<Self::OutputLoader as AssetLoader>::Settings, Self::Error> {1304let serialized = serialize_as_leaf(asset.get().value.clone());1305writer.write_all(serialized.as_bytes()).await1306}1307}13081309let process_counter = Arc::new(Mutex::new(0));13101311type NesterProcessor = LoadTransformAndSave<1312NesterLoader,1313RootAssetTransformer<AddTextToNested, Nester>,1314NesterSaver,1315>;1316app.init_asset::<Nester>()1317.register_asset_loader(NesterLoader)1318.register_asset_processor(NesterProcessor::new(1319RootAssetTransformer::new(AddTextToNested("-ref".into(), process_counter.clone())),1320NesterSaver,1321))1322.set_default_asset_processor::<NesterProcessor>("nest");13231324let guard = source_gate.write_blocking();13251326// This test also checks that processing of nested assets can occur across asset sources.1327custom_source_dir.insert_asset_text(1328Path::new("top.nest"),1329&ron::ser::to_string(&NesterSerialized::Path("middle.nest".into())).unwrap(),1330);1331default_source_dir.insert_asset_text(1332Path::new("middle.nest"),1333&ron::ser::to_string(&NesterSerialized::Path("custom://bottom.nest".into())).unwrap(),1334);1335custom_source_dir1336.insert_asset_text(Path::new("bottom.nest"), &serialize_as_leaf("leaf".into()));1337default_source_dir.insert_asset_text(1338Path::new("unrelated.nest"),1339&serialize_as_leaf("unrelated".into()),1340);13411342run_app_until_finished_processing(&mut app, guard);13431344// The initial processing step should have processed all assets.1345assert_eq!(1346read_asset_as_string(&custom_processed_dir, Path::new("bottom.nest")),1347serialize_as_leaf("leaf-ref".into())1348);1349assert_eq!(1350read_asset_as_string(&default_processed_dir, Path::new("middle.nest")),1351serialize_as_leaf("leaf-ref-ref".into())1352);1353assert_eq!(1354read_asset_as_string(&custom_processed_dir, Path::new("top.nest")),1355serialize_as_leaf("leaf-ref-ref-ref".into())1356);1357assert_eq!(1358read_asset_as_string(&default_processed_dir, Path::new("unrelated.nest")),1359serialize_as_leaf("unrelated-ref".into())1360);13611362let get_process_count = || {1363*process_counter1364.lock()1365.unwrap_or_else(PoisonError::into_inner)1366};1367assert_eq!(get_process_count(), 4);13681369// Now we will only send a single source event, but that should still result in all related1370// assets being reprocessed.1371let guard = source_gate.write_blocking();13721373custom_source_dir.insert_asset_text(1374Path::new("bottom.nest"),1375&serialize_as_leaf("leaf changed".into()),1376);1377custom_source_events1378.send_blocking(AssetSourceEvent::ModifiedAsset("bottom.nest".into()))1379.unwrap();13801381run_app_until_finished_processing(&mut app, guard);13821383assert_eq!(1384read_asset_as_string(&custom_processed_dir, Path::new("bottom.nest")),1385serialize_as_leaf("leaf changed-ref".into())1386);1387assert_eq!(1388read_asset_as_string(&default_processed_dir, Path::new("middle.nest")),1389serialize_as_leaf("leaf changed-ref-ref".into())1390);1391assert_eq!(1392read_asset_as_string(&custom_processed_dir, Path::new("top.nest")),1393serialize_as_leaf("leaf changed-ref-ref-ref".into())1394);1395assert_eq!(1396read_asset_as_string(&default_processed_dir, Path::new("unrelated.nest")),1397serialize_as_leaf("unrelated-ref".into())1398);13991400assert_eq!(get_process_count(), 7);14011402// Send a modify event to the middle asset without changing the asset bytes. This should do1403// **nothing** since neither its dependencies nor its bytes have changed.1404let guard = source_gate.write_blocking();14051406default_source_events1407.send_blocking(AssetSourceEvent::ModifiedAsset("middle.nest".into()))1408.unwrap();14091410run_app_until_finished_processing(&mut app, guard);14111412assert_eq!(1413read_asset_as_string(&custom_processed_dir, Path::new("bottom.nest")),1414serialize_as_leaf("leaf changed-ref".into())1415);1416assert_eq!(1417read_asset_as_string(&default_processed_dir, Path::new("middle.nest")),1418serialize_as_leaf("leaf changed-ref-ref".into())1419);1420assert_eq!(1421read_asset_as_string(&custom_processed_dir, Path::new("top.nest")),1422serialize_as_leaf("leaf changed-ref-ref-ref".into())1423);1424assert_eq!(1425read_asset_as_string(&default_processed_dir, Path::new("unrelated.nest")),1426serialize_as_leaf("unrelated-ref".into())1427);14281429assert_eq!(get_process_count(), 7);1430}14311432#[test]1433fn clears_invalid_data_from_processed_dir() {1434let AppWithProcessor {1435mut app,1436source_gate,1437default_source_dirs:1438ProcessingDirs {1439source: default_source_dir,1440processed: default_processed_dir,1441..1442},1443..1444} = create_app_with_asset_processor(&[]);14451446type CoolTextProcessor = LoadTransformAndSave<1447CoolTextLoader,1448RootAssetTransformer<AddText, CoolText>,1449CoolTextSaver,1450>;1451app.init_asset::<CoolText>()1452.init_asset::<SubText>()1453.register_asset_loader(CoolTextLoader)1454.register_asset_processor(CoolTextProcessor::new(1455RootAssetTransformer::new(AddText(" processed".to_string())),1456CoolTextSaver,1457))1458.set_default_asset_processor::<CoolTextProcessor>("cool.ron");14591460let guard = source_gate.write_blocking();14611462default_source_dir.insert_asset_text(Path::new("a.cool.ron"), &serialize_as_cool_text("a"));1463default_source_dir.insert_asset_text(Path::new("dir/b.cool.ron"), &serialize_as_cool_text("b"));1464default_source_dir.insert_asset_text(1465Path::new("dir/subdir/c.cool.ron"),1466&serialize_as_cool_text("c"),1467);14681469// This asset has the right data, but no meta, so it should be reprocessed.1470let a = Path::new("a.cool.ron");1471default_processed_dir.insert_asset_text(a, &serialize_as_cool_text("a processed"));1472// These assets aren't present in the unprocessed directory, so they should be deleted.1473let missing1 = Path::new("missing1.cool.ron");1474let missing2 = Path::new("dir/missing2.cool.ron");1475let missing3 = Path::new("other_dir/missing3.cool.ron");1476default_processed_dir.insert_asset_text(missing1, &serialize_as_cool_text("missing1"));1477default_processed_dir.insert_meta_text(missing1, ""); // This asset has metadata.1478default_processed_dir.insert_asset_text(missing2, &serialize_as_cool_text("missing2"));1479default_processed_dir.insert_asset_text(missing3, &serialize_as_cool_text("missing3"));1480// This directory is empty, so it should be deleted.1481let empty_dir = Path::new("empty_dir");1482let empty_dir_subdir = Path::new("empty_dir/empty_subdir");1483default_processed_dir.get_or_insert_dir(empty_dir_subdir);14841485run_app_until_finished_processing(&mut app, guard);14861487assert_eq!(1488read_asset_as_string(&default_processed_dir, a),1489serialize_as_cool_text("a processed")1490);1491assert!(default_processed_dir.get_metadata(a).is_some());14921493assert!(default_processed_dir.get_asset(missing1).is_none());1494assert!(default_processed_dir.get_metadata(missing1).is_none());1495assert!(default_processed_dir.get_asset(missing2).is_none());1496assert!(default_processed_dir.get_asset(missing3).is_none());14971498assert!(default_processed_dir.get_dir(empty_dir_subdir).is_none());1499assert!(default_processed_dir.get_dir(empty_dir).is_none());1500}15011502#[test]1503fn only_reprocesses_wrong_hash_on_startup() {1504let no_deps_asset = Path::new("no_deps.cool.ron");1505let source_changed_asset = Path::new("source_changed.cool.ron");1506let dep_unchanged_asset = Path::new("dep_unchanged.cool.ron");1507let dep_changed_asset = Path::new("dep_changed.cool.ron");1508let default_source_dir;1509let default_processed_dir;15101511#[derive(TypePath, Clone)]1512struct MergeEmbeddedAndAddText;15131514impl MutateAsset<CoolText> for MergeEmbeddedAndAddText {1515fn mutate(&self, asset: &mut CoolText) {1516asset.text.push_str(" processed");1517if asset.embedded.is_empty() {1518return;1519}1520asset.text.push(' ');1521asset.text.push_str(&asset.embedded);1522// Clear the embedded text so that saving doesn't break.1523asset.embedded.clear();1524}1525}15261527#[derive(TypePath, Clone)]1528struct Count<T>(Arc<Mutex<u32>>, T);15291530impl<A: Asset, T: MutateAsset<A>> MutateAsset<A> for Count<T> {1531fn mutate(&self, asset: &mut A) {1532*self.0.lock().unwrap_or_else(PoisonError::into_inner) += 1;1533self.1.mutate(asset);1534}1535}15361537let transformer = Count(Arc::new(Mutex::new(0)), MergeEmbeddedAndAddText);1538type CoolTextProcessor = LoadTransformAndSave<1539CoolTextLoader,1540RootAssetTransformer<Count<MergeEmbeddedAndAddText>, CoolText>,1541CoolTextSaver,1542>;15431544// Create a scope so that the app is completely gone afterwards (and we can see what happens1545// after reinitializing).1546{1547let AppWithProcessor {1548mut app,1549source_gate,1550default_source_dirs,1551..1552} = create_app_with_asset_processor(&[]);1553default_source_dir = default_source_dirs.source;1554default_processed_dir = default_source_dirs.processed;15551556app.init_asset::<CoolText>()1557.init_asset::<SubText>()1558.register_asset_loader(CoolTextLoader)1559.register_asset_processor(CoolTextProcessor::new(1560RootAssetTransformer::new(transformer.clone()),1561CoolTextSaver,1562))1563.set_default_asset_processor::<CoolTextProcessor>("cool.ron");15641565let guard = source_gate.write_blocking();15661567let cool_text_with_embedded = |text: &str, embedded: &Path| {1568let cool_text_ron = CoolTextRon {1569text: text.into(),1570dependencies: vec![],1571embedded_dependencies: vec![embedded.to_string_lossy().into_owned()],1572sub_texts: vec![],1573};1574ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()1575};15761577default_source_dir.insert_asset_text(no_deps_asset, &serialize_as_cool_text("no_deps"));1578default_source_dir.insert_asset_text(1579source_changed_asset,1580&serialize_as_cool_text("source_changed"),1581);1582default_source_dir.insert_asset_text(1583dep_unchanged_asset,1584&cool_text_with_embedded("dep_unchanged", no_deps_asset),1585);1586default_source_dir.insert_asset_text(1587dep_changed_asset,1588&cool_text_with_embedded("dep_changed", source_changed_asset),1589);15901591run_app_until_finished_processing(&mut app, guard);15921593assert_eq!(1594read_asset_as_string(&default_processed_dir, no_deps_asset),1595serialize_as_cool_text("no_deps processed")1596);1597assert_eq!(1598read_asset_as_string(&default_processed_dir, source_changed_asset),1599serialize_as_cool_text("source_changed processed")1600);1601assert_eq!(1602read_asset_as_string(&default_processed_dir, dep_unchanged_asset),1603serialize_as_cool_text("dep_unchanged processed no_deps processed")1604);1605assert_eq!(1606read_asset_as_string(&default_processed_dir, dep_changed_asset),1607serialize_as_cool_text("dep_changed processed source_changed processed")1608);1609}16101611// Assert and reset the processing count.1612assert_eq!(1613core::mem::take(&mut *transformer.0.lock().unwrap_or_else(PoisonError::into_inner)),161441615);16161617// Hand-make the app, since we need to pass in our already existing Dirs from the last app.1618let mut app = App::new();1619let source_gate = Arc::new(RwLock::new(()));16201621let source_memory_reader = LockGatedReader::new(1622source_gate.clone(),1623MemoryAssetReader {1624root: default_source_dir.clone(),1625},1626);1627let processed_memory_reader = MemoryAssetReader {1628root: default_processed_dir.clone(),1629};1630let processed_memory_writer = MemoryAssetWriter {1631root: default_processed_dir.clone(),1632};16331634app.register_asset_source(1635AssetSourceId::Default,1636AssetSourceBuilder::new(move || Box::new(source_memory_reader.clone()))1637.with_processed_reader(move || Box::new(processed_memory_reader.clone()))1638.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),1639);16401641app.add_plugins((1642TaskPoolPlugin::default(),1643AssetPlugin {1644mode: AssetMode::Processed,1645use_asset_processor_override: Some(true),1646watch_for_changes_override: Some(true),1647..Default::default()1648},1649));16501651set_fake_transaction_log(&mut app);16521653app.init_asset::<CoolText>()1654.init_asset::<SubText>()1655.register_asset_loader(CoolTextLoader)1656.register_asset_processor(CoolTextProcessor::new(1657RootAssetTransformer::new(transformer.clone()),1658CoolTextSaver,1659))1660.set_default_asset_processor::<CoolTextProcessor>("cool.ron");16611662let guard = source_gate.write_blocking();16631664default_source_dir1665.insert_asset_text(source_changed_asset, &serialize_as_cool_text("DIFFERENT"));16661667run_app_until_finished_processing(&mut app, guard);16681669// Only source_changed and dep_changed assets were reprocessed - all others still have the same1670// hashes.1671let num_processes = *transformer.0.lock().unwrap_or_else(PoisonError::into_inner);1672// TODO: assert_eq! (num_processes == 2) only after we prevent double processing assets1673// == 3 happens when the initial processing of an asset and the re-processing that its dependency1674// triggers are both able to proceed. (dep_changed_asset in this case is processed twice)1675assert!(num_processes == 2 || num_processes == 3);16761677assert_eq!(1678read_asset_as_string(&default_processed_dir, no_deps_asset),1679serialize_as_cool_text("no_deps processed")1680);1681assert_eq!(1682read_asset_as_string(&default_processed_dir, source_changed_asset),1683serialize_as_cool_text("DIFFERENT processed")1684);1685assert_eq!(1686read_asset_as_string(&default_processed_dir, dep_unchanged_asset),1687serialize_as_cool_text("dep_unchanged processed no_deps processed")1688);1689assert_eq!(1690read_asset_as_string(&default_processed_dir, dep_changed_asset),1691serialize_as_cool_text("dep_changed processed DIFFERENT processed")1692);1693}16941695#[test]1696fn writes_default_meta_for_processor() {1697let AppWithProcessor {1698mut app,1699default_source_dirs: ProcessingDirs { source, .. },1700..1701} = create_app_with_asset_processor(&[]);17021703type CoolTextProcessor = LoadTransformAndSave<1704CoolTextLoader,1705RootAssetTransformer<AddText, CoolText>,1706CoolTextSaver,1707>;17081709app.register_asset_processor(CoolTextProcessor::new(1710RootAssetTransformer::new(AddText("blah".to_string())),1711CoolTextSaver,1712))1713.set_default_asset_processor::<CoolTextProcessor>("cool.ron");17141715const ASSET_PATH: &str = "abc.cool.ron";1716source.insert_asset_text(Path::new(ASSET_PATH), &serialize_as_cool_text("blah"));17171718let processor = app.world().resource::<AssetProcessor>().clone();1719bevy_tasks::block_on(processor.write_default_meta_file_for_path(ASSET_PATH)).unwrap();17201721assert_eq!(1722read_meta_as_string(&source, Path::new(ASSET_PATH)),1723r#"(1724meta_format_version: "1.0",1725asset: Process(1726processor: "bevy_asset::processor::process::LoadTransformAndSave<bevy_asset::tests::CoolTextLoader, bevy_asset::processor::tests::RootAssetTransformer<bevy_asset::processor::tests::AddText, bevy_asset::tests::CoolText>, bevy_asset::saver::tests::CoolTextSaver>",1727settings: (1728loader_settings: (),1729transformer_settings: (),1730saver_settings: (),1731),1732),1733)"#1734);1735}17361737#[test]1738fn write_default_meta_does_not_overwrite() {1739let AppWithProcessor {1740mut app,1741default_source_dirs: ProcessingDirs { source, .. },1742..1743} = create_app_with_asset_processor(&[]);17441745type CoolTextProcessor = LoadTransformAndSave<1746CoolTextLoader,1747RootAssetTransformer<AddText, CoolText>,1748CoolTextSaver,1749>;17501751app.register_asset_processor(CoolTextProcessor::new(1752RootAssetTransformer::new(AddText("blah".to_string())),1753CoolTextSaver,1754))1755.set_default_asset_processor::<CoolTextProcessor>("cool.ron");17561757const ASSET_PATH: &str = "abc.cool.ron";1758source.insert_asset_text(Path::new(ASSET_PATH), &serialize_as_cool_text("blah"));1759const META_TEXT: &str = "hey i'm walkin here!";1760source.insert_meta_text(Path::new(ASSET_PATH), META_TEXT);17611762let processor = app.world().resource::<AssetProcessor>().clone();1763assert!(matches!(1764bevy_tasks::block_on(processor.write_default_meta_file_for_path(ASSET_PATH)),1765Err(WriteDefaultMetaError::MetaAlreadyExists)1766));17671768assert_eq!(1769read_meta_as_string(&source, Path::new(ASSET_PATH)),1770META_TEXT1771);1772}177317741775