Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_asset/src/processor/tests.rs
9332 views
1
use alloc::{
2
boxed::Box,
3
collections::BTreeMap,
4
string::{String, ToString},
5
sync::Arc,
6
vec,
7
vec::Vec,
8
};
9
use async_lock::{RwLock, RwLockWriteGuard};
10
use bevy_platform::{
11
collections::HashMap,
12
sync::{Mutex, PoisonError},
13
};
14
use bevy_reflect::TypePath;
15
use core::marker::PhantomData;
16
use futures_lite::AsyncWriteExt;
17
use ron::ser::PrettyConfig;
18
use serde::{Deserialize, Serialize};
19
use std::path::Path;
20
21
use bevy_app::{App, TaskPoolPlugin};
22
use bevy_ecs::error::BevyError;
23
use bevy_tasks::BoxedFuture;
24
25
use crate::{
26
io::{
27
memory::{Dir, MemoryAssetReader, MemoryAssetWriter},
28
AssetReader, AssetReaderError, AssetSourceBuilder, AssetSourceBuilders, AssetSourceEvent,
29
AssetSourceId, AssetWatcher, PathStream, Reader,
30
},
31
processor::{
32
AssetProcessor, GetProcessorError, LoadTransformAndSave, LogEntry, Process, ProcessContext,
33
ProcessError, ProcessorState, ProcessorTransactionLog, ProcessorTransactionLogFactory,
34
},
35
saver::{tests::CoolTextSaver, AssetSaver},
36
tests::{
37
read_asset_as_string, read_meta_as_string, run_app_until, CoolText, CoolTextLoader,
38
CoolTextRon, SubText,
39
},
40
transformer::{AssetTransformer, TransformedAsset},
41
Asset, AssetApp, AssetLoader, AssetMode, AssetPath, AssetPlugin, LoadContext,
42
WriteDefaultMetaError,
43
};
44
45
#[derive(TypePath)]
46
struct MyProcessor<T>(PhantomData<fn() -> T>);
47
48
impl<T: TypePath + 'static> Process for MyProcessor<T> {
49
type OutputLoader = ();
50
type Settings = ();
51
52
async fn process(
53
&self,
54
_context: &mut ProcessContext<'_>,
55
_settings: &Self::Settings,
56
_writer: &mut crate::io::Writer,
57
) -> Result<(), ProcessError> {
58
Ok(())
59
}
60
}
61
62
#[derive(TypePath)]
63
struct Marker;
64
65
fn create_empty_asset_processor() -> AssetProcessor {
66
let mut sources = AssetSourceBuilders::default();
67
// Create an empty asset source so that AssetProcessor is happy.
68
let dir = Dir::default();
69
let memory_reader = MemoryAssetReader { root: dir.clone() };
70
sources.insert(
71
AssetSourceId::Default,
72
AssetSourceBuilder::new(move || Box::new(memory_reader.clone())),
73
);
74
75
AssetProcessor::new(&mut sources, false).0
76
}
77
78
#[test]
79
fn get_asset_processor_by_name() {
80
let asset_processor = create_empty_asset_processor();
81
asset_processor.register_processor(MyProcessor::<Marker>(PhantomData));
82
83
let long_processor = asset_processor
84
.get_processor(
85
"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",
86
)
87
.expect("Processor was previously registered");
88
let short_processor = asset_processor
89
.get_processor("MyProcessor<Marker>")
90
.expect("Processor was previously registered");
91
92
// We can use either the long or short processor name and we will get the same processor
93
// out.
94
assert!(Arc::ptr_eq(&long_processor, &short_processor));
95
}
96
97
#[test]
98
fn missing_processor_returns_error() {
99
let asset_processor = create_empty_asset_processor();
100
101
let Err(long_processor_err) = asset_processor.get_processor(
102
"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",
103
) else {
104
panic!("Processor was returned even though we never registered any.");
105
};
106
let GetProcessorError::Missing(long_processor_err) = &long_processor_err else {
107
panic!("get_processor returned incorrect error: {long_processor_err}");
108
};
109
assert_eq!(
110
long_processor_err,
111
"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>"
112
);
113
114
// Short paths should also return an error.
115
116
let Err(long_processor_err) = asset_processor.get_processor("MyProcessor<Marker>") else {
117
panic!("Processor was returned even though we never registered any.");
118
};
119
let GetProcessorError::Missing(long_processor_err) = &long_processor_err else {
120
panic!("get_processor returned incorrect error: {long_processor_err}");
121
};
122
assert_eq!(long_processor_err, "MyProcessor<Marker>");
123
}
124
125
// Create another marker type whose short name will overlap `Marker`.
126
mod sneaky {
127
use bevy_reflect::TypePath;
128
129
#[derive(TypePath)]
130
pub struct Marker;
131
}
132
133
#[test]
134
fn ambiguous_short_path_returns_error() {
135
let asset_processor = create_empty_asset_processor();
136
asset_processor.register_processor(MyProcessor::<Marker>(PhantomData));
137
asset_processor.register_processor(MyProcessor::<sneaky::Marker>(PhantomData));
138
139
let Err(long_processor_err) = asset_processor.get_processor("MyProcessor<Marker>") else {
140
panic!("Processor was returned even though the short path is ambiguous.");
141
};
142
let GetProcessorError::Ambiguous {
143
processor_short_name,
144
ambiguous_processor_names,
145
} = &long_processor_err
146
else {
147
panic!("get_processor returned incorrect error: {long_processor_err}");
148
};
149
assert_eq!(processor_short_name, "MyProcessor<Marker>");
150
let expected_ambiguous_names = [
151
"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",
152
"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::sneaky::Marker>",
153
];
154
assert_eq!(ambiguous_processor_names, &expected_ambiguous_names);
155
156
let processor_1 = asset_processor
157
.get_processor(
158
"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::Marker>",
159
)
160
.expect("Processor was previously registered");
161
let processor_2 = asset_processor
162
.get_processor(
163
"bevy_asset::processor::tests::MyProcessor<bevy_asset::processor::tests::sneaky::Marker>",
164
)
165
.expect("Processor was previously registered");
166
167
// If we fully specify the paths, we get the two different processors.
168
assert!(!Arc::ptr_eq(&processor_1, &processor_2));
169
}
170
171
#[derive(Clone)]
172
struct ProcessingDirs {
173
source: Dir,
174
processed: Dir,
175
source_event_sender: async_channel::Sender<AssetSourceEvent>,
176
}
177
178
struct AppWithProcessor {
179
app: App,
180
source_gate: Arc<RwLock<()>>,
181
default_source_dirs: ProcessingDirs,
182
extra_sources_dirs: HashMap<String, ProcessingDirs>,
183
}
184
185
/// Similar to [`crate::io::gated::GatedReader`], but uses a lock instead of a channel to avoid
186
/// needing to send the "correct" number of messages.
187
#[derive(Clone)]
188
struct LockGatedReader<R: AssetReader> {
189
reader: R,
190
gate: Arc<RwLock<()>>,
191
}
192
193
impl<R: AssetReader> LockGatedReader<R> {
194
/// Creates a new [`GatedReader`], which wraps the given `reader`. Also returns a [`GateOpener`] which
195
/// can be used to open "path gates" for this [`GatedReader`].
196
fn new(gate: Arc<RwLock<()>>, reader: R) -> Self {
197
Self { gate, reader }
198
}
199
}
200
201
impl<R: AssetReader> AssetReader for LockGatedReader<R> {
202
async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
203
let _guard = self.gate.read().await;
204
self.reader.read(path).await
205
}
206
207
async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
208
let _guard = self.gate.read().await;
209
self.reader.read_meta(path).await
210
}
211
212
async fn read_directory<'a>(
213
&'a self,
214
path: &'a Path,
215
) -> Result<Box<PathStream>, AssetReaderError> {
216
let _guard = self.gate.read().await;
217
self.reader.read_directory(path).await
218
}
219
220
async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
221
let _guard = self.gate.read().await;
222
self.reader.is_directory(path).await
223
}
224
}
225
226
/// Serializes `text` into a `CoolText` that can be loaded.
227
///
228
/// This doesn't support all the features of `CoolText`, so more complex scenarios may require doing
229
/// this manually.
230
fn serialize_as_cool_text(text: &str) -> String {
231
let cool_text_ron = CoolTextRon {
232
text: text.into(),
233
dependencies: vec![],
234
embedded_dependencies: vec![],
235
sub_texts: vec![],
236
};
237
ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
238
}
239
240
/// Sets the transaction log for the app to a fake one to prevent touching the filesystem.
241
fn set_fake_transaction_log(app: &mut App) {
242
/// A dummy transaction log factory that just creates [`FakeTransactionLog`].
243
struct FakeTransactionLogFactory;
244
245
impl ProcessorTransactionLogFactory for FakeTransactionLogFactory {
246
fn read(&self) -> BoxedFuture<'_, Result<Vec<LogEntry>, BevyError>> {
247
Box::pin(async move { Ok(vec![]) })
248
}
249
250
fn create_new_log(
251
&self,
252
) -> BoxedFuture<'_, Result<Box<dyn ProcessorTransactionLog>, BevyError>> {
253
Box::pin(async move { Ok(Box::new(FakeTransactionLog) as _) })
254
}
255
}
256
257
/// A dummy transaction log that just drops every log.
258
// TODO: In the future it's possible for us to have a test of the transaction log, so making
259
// this more complex may be necessary.
260
struct FakeTransactionLog;
261
262
impl ProcessorTransactionLog for FakeTransactionLog {
263
fn begin_processing<'a>(
264
&'a mut self,
265
_asset: &'a AssetPath<'_>,
266
) -> BoxedFuture<'a, Result<(), BevyError>> {
267
Box::pin(async move { Ok(()) })
268
}
269
270
fn end_processing<'a>(
271
&'a mut self,
272
_asset: &'a AssetPath<'_>,
273
) -> BoxedFuture<'a, Result<(), BevyError>> {
274
Box::pin(async move { Ok(()) })
275
}
276
277
fn unrecoverable(&mut self) -> BoxedFuture<'_, Result<(), BevyError>> {
278
Box::pin(async move { Ok(()) })
279
}
280
}
281
282
app.world()
283
.resource::<AssetProcessor>()
284
.data()
285
.set_log_factory(Box::new(FakeTransactionLogFactory))
286
.unwrap();
287
}
288
289
fn create_app_with_asset_processor(extra_sources: &[String]) -> AppWithProcessor {
290
let mut app = App::new();
291
let source_gate = Arc::new(RwLock::new(()));
292
293
struct UnfinishedProcessingDirs {
294
source: Dir,
295
processed: Dir,
296
// The receiver channel for the source event sender for the unprocessed source.
297
source_event_sender_receiver:
298
async_channel::Receiver<async_channel::Sender<AssetSourceEvent>>,
299
}
300
301
impl UnfinishedProcessingDirs {
302
fn finish(self) -> ProcessingDirs {
303
ProcessingDirs {
304
source: self.source,
305
processed: self.processed,
306
// The processor listens for events on the source unconditionally, and we enable
307
// watching for the processed source, so both of these channels will be filled.
308
source_event_sender: self.source_event_sender_receiver.recv_blocking().unwrap(),
309
}
310
}
311
}
312
313
fn create_source(
314
app: &mut App,
315
source_id: AssetSourceId<'static>,
316
source_gate: Arc<RwLock<()>>,
317
) -> UnfinishedProcessingDirs {
318
let source_dir = Dir::default();
319
let processed_dir = Dir::default();
320
321
let source_memory_reader = LockGatedReader::new(
322
source_gate,
323
MemoryAssetReader {
324
root: source_dir.clone(),
325
},
326
);
327
let source_memory_writer = MemoryAssetWriter {
328
root: source_dir.clone(),
329
};
330
let processed_memory_reader = MemoryAssetReader {
331
root: processed_dir.clone(),
332
};
333
let processed_memory_writer = MemoryAssetWriter {
334
root: processed_dir.clone(),
335
};
336
337
let (source_event_sender_sender, source_event_sender_receiver) = async_channel::bounded(1);
338
339
struct FakeWatcher;
340
341
impl AssetWatcher for FakeWatcher {}
342
343
app.register_asset_source(
344
source_id,
345
AssetSourceBuilder::new(move || Box::new(source_memory_reader.clone()))
346
.with_writer(move |_| Some(Box::new(source_memory_writer.clone())))
347
.with_watcher(move |sender: async_channel::Sender<AssetSourceEvent>| {
348
source_event_sender_sender.send_blocking(sender).unwrap();
349
Some(Box::new(FakeWatcher))
350
})
351
.with_processed_reader(move || Box::new(processed_memory_reader.clone()))
352
.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),
353
);
354
355
UnfinishedProcessingDirs {
356
source: source_dir,
357
processed: processed_dir,
358
source_event_sender_receiver,
359
}
360
}
361
362
let default_source_dirs = create_source(&mut app, AssetSourceId::Default, source_gate.clone());
363
364
let extra_sources_dirs = extra_sources
365
.iter()
366
.map(|source_name| {
367
(
368
source_name.clone(),
369
create_source(
370
&mut app,
371
AssetSourceId::Name(source_name.clone().into()),
372
source_gate.clone(),
373
),
374
)
375
})
376
.collect::<Vec<_>>();
377
378
app.add_plugins((
379
TaskPoolPlugin::default(),
380
AssetPlugin {
381
mode: AssetMode::Processed,
382
use_asset_processor_override: Some(true),
383
watch_for_changes_override: Some(true),
384
..Default::default()
385
},
386
));
387
388
set_fake_transaction_log(&mut app);
389
390
// Now that we've built the app, finish all the processing dirs.
391
392
AppWithProcessor {
393
app,
394
source_gate,
395
default_source_dirs: default_source_dirs.finish(),
396
extra_sources_dirs: extra_sources_dirs
397
.into_iter()
398
.map(|(name, dirs)| (name, dirs.finish()))
399
.collect(),
400
}
401
}
402
403
fn run_app_until_finished_processing(app: &mut App, guard: RwLockWriteGuard<'_, ()>) {
404
let processor = app.world().resource::<AssetProcessor>().clone();
405
// We can't just wait for the processor state to be finished since we could have already
406
// finished before, but now that something has changed, we may not have restarted processing
407
// yet. So wait for processing to start, then finish.
408
run_app_until(app, |_| {
409
// Before we even consider whether the processor is started, make sure that none of the
410
// receivers have anything left in them. This prevents us accidentally, considering the
411
// processor as processing before all the events have been processed.
412
for source in processor.sources().iter() {
413
let Some(recv) = source.event_receiver() else {
414
continue;
415
};
416
if !recv.is_empty() {
417
return None;
418
}
419
}
420
let state = bevy_tasks::block_on(processor.get_state());
421
(state == ProcessorState::Processing || state == ProcessorState::Initializing).then_some(())
422
});
423
drop(guard);
424
run_app_until(app, |_| {
425
(bevy_tasks::block_on(processor.get_state()) == ProcessorState::Finished).then_some(())
426
});
427
}
428
429
// Note: while we allow any Fn, since closures are unnameable types, creating a processor with a
430
// closure cannot be used (since we need to include the name of the transformer in the meta
431
// file).
432
#[derive(TypePath)]
433
struct RootAssetTransformer<M: MutateAsset<A>, A: Asset>(M, PhantomData<fn(&mut A)>);
434
435
trait MutateAsset<A: Asset>: TypePath + Send + Sync + 'static {
436
fn mutate(&self, asset: &mut A);
437
}
438
439
impl<M: MutateAsset<A>, A: Asset> RootAssetTransformer<M, A> {
440
fn new(m: M) -> Self {
441
Self(m, PhantomData)
442
}
443
}
444
445
impl<M: MutateAsset<A>, A: Asset> AssetTransformer for RootAssetTransformer<M, A> {
446
type AssetInput = A;
447
type AssetOutput = A;
448
type Error = std::io::Error;
449
type Settings = ();
450
451
async fn transform<'a>(
452
&'a self,
453
mut asset: TransformedAsset<A>,
454
_settings: &'a Self::Settings,
455
) -> Result<TransformedAsset<A>, Self::Error> {
456
self.0.mutate(asset.get_mut());
457
Ok(asset)
458
}
459
}
460
461
#[derive(TypePath)]
462
struct AddText(String);
463
464
impl MutateAsset<CoolText> for AddText {
465
fn mutate(&self, text: &mut CoolText) {
466
text.text.push_str(&self.0);
467
}
468
}
469
470
#[test]
471
fn no_meta_or_default_processor_copies_asset() {
472
// Assets without a meta file or a default processor should still be accessible in the
473
// processed path. Note: This isn't exactly the desired property - we don't want the assets
474
// to be copied to the processed directory. We just want these assets to still be loadable
475
// if we no longer have the source directory. This could be done with a symlink instead of a
476
// copy.
477
478
let AppWithProcessor {
479
mut app,
480
source_gate,
481
default_source_dirs:
482
ProcessingDirs {
483
source: source_dir,
484
processed: processed_dir,
485
..
486
},
487
..
488
} = create_app_with_asset_processor(&[]);
489
490
let guard = source_gate.write_blocking();
491
492
let path = Path::new("abc.cool.ron");
493
let source_asset = r#"(
494
text: "abc",
495
dependencies: [],
496
embedded_dependencies: [],
497
sub_texts: [],
498
)"#;
499
500
source_dir.insert_asset_text(path, source_asset);
501
502
run_app_until_finished_processing(&mut app, guard);
503
504
let processed_asset = processed_dir.get_asset(path).unwrap();
505
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
506
assert_eq!(processed_asset, source_asset);
507
}
508
509
#[test]
510
fn asset_processor_transforms_asset_default_processor() {
511
let AppWithProcessor {
512
mut app,
513
source_gate,
514
default_source_dirs:
515
ProcessingDirs {
516
source: source_dir,
517
processed: processed_dir,
518
..
519
},
520
..
521
} = create_app_with_asset_processor(&[]);
522
523
type CoolTextProcessor = LoadTransformAndSave<
524
CoolTextLoader,
525
RootAssetTransformer<AddText, CoolText>,
526
CoolTextSaver,
527
>;
528
app.register_asset_loader(CoolTextLoader)
529
.register_asset_processor(CoolTextProcessor::new(
530
RootAssetTransformer::new(AddText("_def".into())),
531
CoolTextSaver,
532
))
533
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
534
535
let guard = source_gate.write_blocking();
536
537
let path = Path::new("abc.cool.ron");
538
source_dir.insert_asset_text(
539
path,
540
r#"(
541
text: "abc",
542
dependencies: [],
543
embedded_dependencies: [],
544
sub_texts: [],
545
)"#,
546
);
547
548
run_app_until_finished_processing(&mut app, guard);
549
550
let processed_asset = processed_dir.get_asset(path).unwrap();
551
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
552
assert_eq!(
553
processed_asset,
554
r#"(
555
text: "abc_def",
556
dependencies: [],
557
embedded_dependencies: [],
558
sub_texts: [],
559
)"#
560
);
561
}
562
563
#[test]
564
fn asset_processor_transforms_asset_with_meta() {
565
let AppWithProcessor {
566
mut app,
567
source_gate,
568
default_source_dirs:
569
ProcessingDirs {
570
source: source_dir,
571
processed: processed_dir,
572
..
573
},
574
..
575
} = create_app_with_asset_processor(&[]);
576
577
type CoolTextProcessor = LoadTransformAndSave<
578
CoolTextLoader,
579
RootAssetTransformer<AddText, CoolText>,
580
CoolTextSaver,
581
>;
582
app.register_asset_loader(CoolTextLoader)
583
.register_asset_processor(CoolTextProcessor::new(
584
RootAssetTransformer::new(AddText("_def".into())),
585
CoolTextSaver,
586
));
587
588
let guard = source_gate.write_blocking();
589
590
let path = Path::new("abc.cool.ron");
591
source_dir.insert_asset_text(
592
path,
593
r#"(
594
text: "abc",
595
dependencies: [],
596
embedded_dependencies: [],
597
sub_texts: [],
598
)"#,
599
);
600
source_dir.insert_meta_text(path, r#"(
601
meta_format_version: "1.0",
602
asset: Process(
603
processor: "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>",
604
settings: (
605
loader_settings: (),
606
transformer_settings: (),
607
saver_settings: (),
608
),
609
),
610
)"#);
611
612
run_app_until_finished_processing(&mut app, guard);
613
614
let processed_asset = processed_dir.get_asset(path).unwrap();
615
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
616
assert_eq!(
617
processed_asset,
618
r#"(
619
text: "abc_def",
620
dependencies: [],
621
embedded_dependencies: [],
622
sub_texts: [],
623
)"#
624
);
625
}
626
627
#[test]
628
fn asset_processor_transforms_asset_with_short_path_meta() {
629
let AppWithProcessor {
630
mut app,
631
source_gate,
632
default_source_dirs:
633
ProcessingDirs {
634
source: source_dir,
635
processed: processed_dir,
636
..
637
},
638
..
639
} = create_app_with_asset_processor(&[]);
640
641
type CoolTextProcessor = LoadTransformAndSave<
642
CoolTextLoader,
643
RootAssetTransformer<AddText, CoolText>,
644
CoolTextSaver,
645
>;
646
app.register_asset_loader(CoolTextLoader)
647
.register_asset_processor(CoolTextProcessor::new(
648
RootAssetTransformer::new(AddText("_def".into())),
649
CoolTextSaver,
650
));
651
652
let guard = source_gate.write_blocking();
653
654
let path = Path::new("abc.cool.ron");
655
source_dir.insert_asset_text(
656
path,
657
r#"(
658
text: "abc",
659
dependencies: [],
660
embedded_dependencies: [],
661
sub_texts: [],
662
)"#,
663
);
664
source_dir.insert_meta_text(path, r#"(
665
meta_format_version: "1.0",
666
asset: Process(
667
processor: "LoadTransformAndSave<CoolTextLoader, RootAssetTransformer<AddText, CoolText>, CoolTextSaver>",
668
settings: (
669
loader_settings: (),
670
transformer_settings: (),
671
saver_settings: (),
672
),
673
),
674
)"#);
675
676
run_app_until_finished_processing(&mut app, guard);
677
678
let processed_asset = processed_dir.get_asset(path).unwrap();
679
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
680
assert_eq!(
681
processed_asset,
682
r#"(
683
text: "abc_def",
684
dependencies: [],
685
embedded_dependencies: [],
686
sub_texts: [],
687
)"#
688
);
689
}
690
691
#[derive(Asset, TypePath, Serialize, Deserialize)]
692
struct FakeGltf {
693
gltf_nodes: BTreeMap<String, String>,
694
}
695
696
#[derive(TypePath)]
697
struct FakeGltfLoader;
698
699
impl AssetLoader for FakeGltfLoader {
700
type Asset = FakeGltf;
701
type Settings = ();
702
type Error = std::io::Error;
703
704
async fn load(
705
&self,
706
reader: &mut dyn Reader,
707
_settings: &Self::Settings,
708
_load_context: &mut LoadContext<'_>,
709
) -> Result<Self::Asset, Self::Error> {
710
use std::io::{Error, ErrorKind};
711
712
let mut bytes = vec![];
713
reader.read_to_end(&mut bytes).await?;
714
ron::de::from_bytes(&bytes)
715
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))
716
}
717
718
fn extensions(&self) -> &[&str] {
719
&["gltf"]
720
}
721
}
722
723
#[derive(Asset, TypePath, Serialize, Deserialize)]
724
struct FakeBsn {
725
parent_bsn: Option<String>,
726
nodes: BTreeMap<String, String>,
727
}
728
729
// This loader loads the BSN but as an "inlined" scene. We read the original BSN and create a
730
// scene that holds all the data including parents.
731
// TODO: It would be nice if the inlining was actually done as an `AssetTransformer`, but
732
// `Process` currently has no way to load nested assets.
733
#[derive(TypePath)]
734
struct FakeBsnLoader;
735
736
impl AssetLoader for FakeBsnLoader {
737
type Asset = FakeBsn;
738
type Settings = ();
739
type Error = std::io::Error;
740
741
async fn load(
742
&self,
743
reader: &mut dyn Reader,
744
_settings: &Self::Settings,
745
load_context: &mut LoadContext<'_>,
746
) -> Result<Self::Asset, Self::Error> {
747
use std::io::{Error, ErrorKind};
748
749
let mut bytes = vec![];
750
reader.read_to_end(&mut bytes).await?;
751
let bsn: FakeBsn = ron::de::from_bytes(&bytes)
752
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;
753
754
if bsn.parent_bsn.is_none() {
755
return Ok(bsn);
756
}
757
758
let parent_bsn = bsn.parent_bsn.unwrap();
759
let parent_bsn = load_context
760
.loader()
761
.immediate()
762
.load(parent_bsn)
763
.await
764
.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
765
let mut new_bsn: FakeBsn = parent_bsn.take();
766
for (name, node) in bsn.nodes {
767
new_bsn.nodes.insert(name, node);
768
}
769
Ok(new_bsn)
770
}
771
772
fn extensions(&self) -> &[&str] {
773
&["bsn"]
774
}
775
}
776
777
#[derive(TypePath)]
778
struct GltfToBsn;
779
780
impl AssetTransformer for GltfToBsn {
781
type AssetInput = FakeGltf;
782
type AssetOutput = FakeBsn;
783
type Settings = ();
784
type Error = std::io::Error;
785
786
async fn transform<'a>(
787
&'a self,
788
mut asset: TransformedAsset<Self::AssetInput>,
789
_settings: &'a Self::Settings,
790
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
791
let bsn = FakeBsn {
792
parent_bsn: None,
793
// Pretend we converted all the glTF nodes into BSN's format.
794
nodes: core::mem::take(&mut asset.get_mut().gltf_nodes),
795
};
796
Ok(asset.replace_asset(bsn))
797
}
798
}
799
800
#[derive(TypePath)]
801
struct FakeBsnSaver;
802
803
impl AssetSaver for FakeBsnSaver {
804
type Asset = FakeBsn;
805
type Error = std::io::Error;
806
type OutputLoader = FakeBsnLoader;
807
type Settings = ();
808
809
async fn save(
810
&self,
811
writer: &mut crate::io::Writer,
812
asset: crate::saver::SavedAsset<'_, '_, Self::Asset>,
813
_settings: &Self::Settings,
814
) -> Result<(), Self::Error> {
815
use std::io::{Error, ErrorKind};
816
817
let ron_string =
818
ron::ser::to_string_pretty(asset.get(), PrettyConfig::new().new_line("\n"))
819
.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
820
821
writer.write_all(ron_string.as_bytes()).await
822
}
823
}
824
#[test]
825
fn asset_processor_loading_can_read_processed_assets() {
826
use crate::transformer::IdentityAssetTransformer;
827
828
let AppWithProcessor {
829
mut app,
830
source_gate,
831
default_source_dirs:
832
ProcessingDirs {
833
source: source_dir,
834
processed: processed_dir,
835
..
836
},
837
..
838
} = create_app_with_asset_processor(&[]);
839
840
// This processor loads a gltf file, converts it to BSN and then saves out the BSN.
841
type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;
842
// This processor loads a BSN file (which "inlines" parent BSNs at load), and then saves the
843
// inlined BSN.
844
type BsnProcessor =
845
LoadTransformAndSave<FakeBsnLoader, IdentityAssetTransformer<FakeBsn>, FakeBsnSaver>;
846
app.register_asset_loader(FakeBsnLoader)
847
.register_asset_loader(FakeGltfLoader)
848
.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))
849
.register_asset_processor(BsnProcessor::new(
850
IdentityAssetTransformer::new(),
851
FakeBsnSaver,
852
))
853
.set_default_asset_processor::<GltfProcessor>("gltf")
854
.set_default_asset_processor::<BsnProcessor>("bsn");
855
856
let guard = source_gate.write_blocking();
857
858
let gltf_path = Path::new("abc.gltf");
859
source_dir.insert_asset_text(
860
gltf_path,
861
r#"(
862
gltf_nodes: {
863
"name": "thing",
864
"position": "123",
865
}
866
)"#,
867
);
868
let bsn_path = Path::new("def.bsn");
869
// The bsn tries to load the gltf as a bsn. This only works if the bsn can read processed
870
// assets.
871
source_dir.insert_asset_text(
872
bsn_path,
873
r#"(
874
parent_bsn: Some("abc.gltf"),
875
nodes: {
876
"position": "456",
877
"color": "red",
878
},
879
)"#,
880
);
881
882
run_app_until_finished_processing(&mut app, guard);
883
884
let processed_bsn = processed_dir.get_asset(bsn_path).unwrap();
885
let processed_bsn = str::from_utf8(processed_bsn.value()).unwrap();
886
// The processed bsn should have been "inlined", so no parent and "overlaid" nodes.
887
assert_eq!(
888
processed_bsn,
889
r#"(
890
parent_bsn: None,
891
nodes: {
892
"color": "red",
893
"name": "thing",
894
"position": "456",
895
},
896
)"#
897
);
898
}
899
900
#[test]
901
fn asset_processor_loading_can_read_source_assets() {
902
let AppWithProcessor {
903
mut app,
904
source_gate,
905
default_source_dirs:
906
ProcessingDirs {
907
source: source_dir,
908
processed: processed_dir,
909
..
910
},
911
..
912
} = create_app_with_asset_processor(&[]);
913
914
#[derive(Serialize, Deserialize)]
915
struct FakeGltfxData {
916
// These are the file paths to the gltfs.
917
gltfs: Vec<String>,
918
}
919
920
#[derive(Asset, TypePath)]
921
struct FakeGltfx {
922
gltfs: Vec<FakeGltf>,
923
}
924
925
#[derive(TypePath)]
926
struct FakeGltfxLoader;
927
928
impl AssetLoader for FakeGltfxLoader {
929
type Asset = FakeGltfx;
930
type Error = std::io::Error;
931
type Settings = ();
932
933
async fn load(
934
&self,
935
reader: &mut dyn Reader,
936
_settings: &Self::Settings,
937
load_context: &mut LoadContext<'_>,
938
) -> Result<Self::Asset, Self::Error> {
939
use std::io::{Error, ErrorKind};
940
941
let mut buf = vec![];
942
reader.read_to_end(&mut buf).await?;
943
944
let gltfx_data: FakeGltfxData =
945
ron::de::from_bytes(&buf).map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
946
947
let mut gltfs = vec![];
948
for gltf in gltfx_data.gltfs.into_iter() {
949
// gltfx files come from "generic" software that doesn't know anything about
950
// Bevy, so it needs to load the source assets to make sense.
951
let gltf = load_context
952
.loader()
953
.immediate()
954
.load(gltf)
955
.await
956
.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
957
gltfs.push(gltf.take());
958
}
959
960
Ok(FakeGltfx { gltfs })
961
}
962
963
fn extensions(&self) -> &[&str] {
964
&["gltfx"]
965
}
966
}
967
968
#[derive(TypePath)]
969
struct GltfxToBsn;
970
971
impl AssetTransformer for GltfxToBsn {
972
type AssetInput = FakeGltfx;
973
type AssetOutput = FakeBsn;
974
type Settings = ();
975
type Error = std::io::Error;
976
977
async fn transform<'a>(
978
&'a self,
979
mut asset: TransformedAsset<Self::AssetInput>,
980
_settings: &'a Self::Settings,
981
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
982
let gltfx = asset.get_mut();
983
984
// Merge together all the gltfs from the gltfx into one big bsn.
985
let bsn = gltfx.gltfs.drain(..).fold(
986
FakeBsn {
987
parent_bsn: None,
988
nodes: Default::default(),
989
},
990
|mut bsn, gltf| {
991
for (key, value) in gltf.gltf_nodes {
992
bsn.nodes.insert(key, value);
993
}
994
bsn
995
},
996
);
997
998
Ok(asset.replace_asset(bsn))
999
}
1000
}
1001
1002
// This processor loads a gltf file, converts it to BSN and then saves out the BSN.
1003
type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;
1004
// This processor loads a gltfx file (including its gltf files) and converts it to BSN.
1005
type GltfxProcessor = LoadTransformAndSave<FakeGltfxLoader, GltfxToBsn, FakeBsnSaver>;
1006
app.register_asset_loader(FakeGltfLoader)
1007
.register_asset_loader(FakeGltfxLoader)
1008
.register_asset_loader(FakeBsnLoader)
1009
.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))
1010
.register_asset_processor(GltfxProcessor::new(GltfxToBsn, FakeBsnSaver))
1011
.set_default_asset_processor::<GltfProcessor>("gltf")
1012
.set_default_asset_processor::<GltfxProcessor>("gltfx");
1013
1014
let guard = source_gate.write_blocking();
1015
1016
let gltf_path_1 = Path::new("abc.gltf");
1017
source_dir.insert_asset_text(
1018
gltf_path_1,
1019
r#"(
1020
gltf_nodes: {
1021
"name": "thing",
1022
"position": "123",
1023
}
1024
)"#,
1025
);
1026
let gltf_path_2 = Path::new("def.gltf");
1027
source_dir.insert_asset_text(
1028
gltf_path_2,
1029
r#"(
1030
gltf_nodes: {
1031
"velocity": "456",
1032
"color": "red",
1033
}
1034
)"#,
1035
);
1036
1037
let gltfx_path = Path::new("xyz.gltfx");
1038
source_dir.insert_asset_text(
1039
gltfx_path,
1040
r#"(
1041
gltfs: ["abc.gltf", "def.gltf"],
1042
)"#,
1043
);
1044
1045
run_app_until_finished_processing(&mut app, guard);
1046
1047
// Sanity check that the two gltf files were actually processed.
1048
let processed_gltf_1 = processed_dir.get_asset(gltf_path_1).unwrap();
1049
let processed_gltf_1 = str::from_utf8(processed_gltf_1.value()).unwrap();
1050
assert_eq!(
1051
processed_gltf_1,
1052
r#"(
1053
parent_bsn: None,
1054
nodes: {
1055
"name": "thing",
1056
"position": "123",
1057
},
1058
)"#
1059
);
1060
let processed_gltf_2 = processed_dir.get_asset(gltf_path_2).unwrap();
1061
let processed_gltf_2 = str::from_utf8(processed_gltf_2.value()).unwrap();
1062
assert_eq!(
1063
processed_gltf_2,
1064
r#"(
1065
parent_bsn: None,
1066
nodes: {
1067
"color": "red",
1068
"velocity": "456",
1069
},
1070
)"#
1071
);
1072
1073
// The processed gltfx should have been able to load and merge the gltfs despite them having
1074
// been processed into bsn.
1075
1076
// Blocked on https://github.com/bevyengine/bevy/issues/21269. This is the actual assertion.
1077
// let processed_gltfx = processed_dir.get_asset(gltfx_path).unwrap();
1078
// let processed_gltfx = str::from_utf8(processed_gltfx.value()).unwrap();
1079
// assert_eq!(
1080
// processed_gltfx,
1081
// r#"(
1082
// parent_bsn: None,
1083
// nodes: {
1084
// "color": "red",
1085
// "name": "thing",
1086
// "position": "123",
1087
// "velocity": "456",
1088
// },
1089
// )"#
1090
// );
1091
1092
// This assertion exists to "prove" that this problem exists.
1093
assert!(processed_dir.get_asset(gltfx_path).is_none());
1094
}
1095
1096
#[test]
1097
fn asset_processor_processes_all_sources() {
1098
let AppWithProcessor {
1099
mut app,
1100
source_gate,
1101
default_source_dirs:
1102
ProcessingDirs {
1103
source: default_source_dir,
1104
processed: default_processed_dir,
1105
source_event_sender: default_source_events,
1106
},
1107
extra_sources_dirs,
1108
} = create_app_with_asset_processor(&["custom_1".into(), "custom_2".into()]);
1109
let ProcessingDirs {
1110
source: custom_1_source_dir,
1111
processed: custom_1_processed_dir,
1112
source_event_sender: custom_1_source_events,
1113
} = extra_sources_dirs["custom_1"].clone();
1114
let ProcessingDirs {
1115
source: custom_2_source_dir,
1116
processed: custom_2_processed_dir,
1117
source_event_sender: custom_2_source_events,
1118
} = extra_sources_dirs["custom_2"].clone();
1119
1120
type AddTextProcessor = LoadTransformAndSave<
1121
CoolTextLoader,
1122
RootAssetTransformer<AddText, CoolText>,
1123
CoolTextSaver,
1124
>;
1125
app.init_asset::<CoolText>()
1126
.init_asset::<SubText>()
1127
.register_asset_loader(CoolTextLoader)
1128
.register_asset_processor(AddTextProcessor::new(
1129
RootAssetTransformer::new(AddText(" processed".into())),
1130
CoolTextSaver,
1131
))
1132
.set_default_asset_processor::<AddTextProcessor>("cool.ron");
1133
1134
let guard = source_gate.write_blocking();
1135
1136
// All the assets will have the same path, but they will still be separately processed since
1137
// they are in different sources.
1138
let path = Path::new("asset.cool.ron");
1139
default_source_dir.insert_asset_text(path, &serialize_as_cool_text("default asset"));
1140
custom_1_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 1 asset"));
1141
custom_2_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 2 asset"));
1142
1143
run_app_until_finished_processing(&mut app, guard);
1144
1145
// Check that all the assets are processed.
1146
assert_eq!(
1147
read_asset_as_string(&default_processed_dir, path),
1148
serialize_as_cool_text("default asset processed")
1149
);
1150
assert_eq!(
1151
read_asset_as_string(&custom_1_processed_dir, path),
1152
serialize_as_cool_text("custom 1 asset processed")
1153
);
1154
assert_eq!(
1155
read_asset_as_string(&custom_2_processed_dir, path),
1156
serialize_as_cool_text("custom 2 asset processed")
1157
);
1158
1159
let guard = source_gate.write_blocking();
1160
1161
// Update the default source asset and notify the watcher.
1162
default_source_dir.insert_asset_text(path, &serialize_as_cool_text("default asset changed"));
1163
default_source_events
1164
.send_blocking(AssetSourceEvent::ModifiedAsset(path.to_path_buf()))
1165
.unwrap();
1166
1167
run_app_until_finished_processing(&mut app, guard);
1168
1169
// Check that all the assets are processed again.
1170
assert_eq!(
1171
read_asset_as_string(&default_processed_dir, path),
1172
serialize_as_cool_text("default asset changed processed")
1173
);
1174
assert_eq!(
1175
read_asset_as_string(&custom_1_processed_dir, path),
1176
serialize_as_cool_text("custom 1 asset processed")
1177
);
1178
assert_eq!(
1179
read_asset_as_string(&custom_2_processed_dir, path),
1180
serialize_as_cool_text("custom 2 asset processed")
1181
);
1182
1183
let guard = source_gate.write_blocking();
1184
1185
// Update the custom source assets and notify the watchers.
1186
custom_1_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 1 asset changed"));
1187
custom_2_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 2 asset changed"));
1188
custom_1_source_events
1189
.send_blocking(AssetSourceEvent::ModifiedAsset(path.to_path_buf()))
1190
.unwrap();
1191
custom_2_source_events
1192
.send_blocking(AssetSourceEvent::ModifiedAsset(path.to_path_buf()))
1193
.unwrap();
1194
1195
run_app_until_finished_processing(&mut app, guard);
1196
1197
// Check that all the assets are processed again.
1198
assert_eq!(
1199
read_asset_as_string(&default_processed_dir, path),
1200
serialize_as_cool_text("default asset changed processed")
1201
);
1202
assert_eq!(
1203
read_asset_as_string(&custom_1_processed_dir, path),
1204
serialize_as_cool_text("custom 1 asset changed processed")
1205
);
1206
assert_eq!(
1207
read_asset_as_string(&custom_2_processed_dir, path),
1208
serialize_as_cool_text("custom 2 asset changed processed")
1209
);
1210
}
1211
1212
#[test]
1213
fn nested_loads_of_processed_asset_reprocesses_on_reload() {
1214
let AppWithProcessor {
1215
mut app,
1216
source_gate,
1217
default_source_dirs:
1218
ProcessingDirs {
1219
source: default_source_dir,
1220
processed: default_processed_dir,
1221
source_event_sender: default_source_events,
1222
},
1223
extra_sources_dirs,
1224
} = create_app_with_asset_processor(&["custom".into()]);
1225
let ProcessingDirs {
1226
source: custom_source_dir,
1227
processed: custom_processed_dir,
1228
source_event_sender: custom_source_events,
1229
} = extra_sources_dirs["custom"].clone();
1230
1231
#[derive(Serialize, Deserialize)]
1232
enum NesterSerialized {
1233
Leaf(String),
1234
Path(String),
1235
}
1236
1237
#[derive(Asset, TypePath)]
1238
struct Nester {
1239
value: String,
1240
}
1241
1242
#[derive(TypePath)]
1243
struct NesterLoader;
1244
1245
impl AssetLoader for NesterLoader {
1246
type Asset = Nester;
1247
type Settings = ();
1248
type Error = std::io::Error;
1249
1250
async fn load(
1251
&self,
1252
reader: &mut dyn Reader,
1253
_settings: &Self::Settings,
1254
load_context: &mut LoadContext<'_>,
1255
) -> Result<Self::Asset, Self::Error> {
1256
let mut bytes = vec![];
1257
reader.read_to_end(&mut bytes).await?;
1258
1259
let serialized: NesterSerialized = ron::de::from_bytes(&bytes).unwrap();
1260
Ok(match serialized {
1261
NesterSerialized::Leaf(value) => Nester { value },
1262
NesterSerialized::Path(path) => {
1263
let loaded_asset = load_context.loader().immediate().load(path).await.unwrap();
1264
loaded_asset.take()
1265
}
1266
})
1267
}
1268
1269
fn extensions(&self) -> &[&str] {
1270
&["nest"]
1271
}
1272
}
1273
1274
#[derive(TypePath)]
1275
struct AddTextToNested(String, Arc<Mutex<u32>>);
1276
1277
impl MutateAsset<Nester> for AddTextToNested {
1278
fn mutate(&self, asset: &mut Nester) {
1279
asset.value.push_str(&self.0);
1280
1281
*self.1.lock().unwrap_or_else(PoisonError::into_inner) += 1;
1282
}
1283
}
1284
1285
fn serialize_as_leaf(value: String) -> String {
1286
let serialized = NesterSerialized::Leaf(value);
1287
ron::ser::to_string(&serialized).unwrap()
1288
}
1289
1290
#[derive(TypePath)]
1291
struct NesterSaver;
1292
1293
impl AssetSaver for NesterSaver {
1294
type Asset = Nester;
1295
type Error = std::io::Error;
1296
type Settings = ();
1297
type OutputLoader = NesterLoader;
1298
1299
async fn save(
1300
&self,
1301
writer: &mut crate::io::Writer,
1302
asset: crate::saver::SavedAsset<'_, '_, Self::Asset>,
1303
_settings: &Self::Settings,
1304
) -> Result<<Self::OutputLoader as AssetLoader>::Settings, Self::Error> {
1305
let serialized = serialize_as_leaf(asset.get().value.clone());
1306
writer.write_all(serialized.as_bytes()).await
1307
}
1308
}
1309
1310
let process_counter = Arc::new(Mutex::new(0));
1311
1312
type NesterProcessor = LoadTransformAndSave<
1313
NesterLoader,
1314
RootAssetTransformer<AddTextToNested, Nester>,
1315
NesterSaver,
1316
>;
1317
app.init_asset::<Nester>()
1318
.register_asset_loader(NesterLoader)
1319
.register_asset_processor(NesterProcessor::new(
1320
RootAssetTransformer::new(AddTextToNested("-ref".into(), process_counter.clone())),
1321
NesterSaver,
1322
))
1323
.set_default_asset_processor::<NesterProcessor>("nest");
1324
1325
let guard = source_gate.write_blocking();
1326
1327
// This test also checks that processing of nested assets can occur across asset sources.
1328
custom_source_dir.insert_asset_text(
1329
Path::new("top.nest"),
1330
&ron::ser::to_string(&NesterSerialized::Path("middle.nest".into())).unwrap(),
1331
);
1332
default_source_dir.insert_asset_text(
1333
Path::new("middle.nest"),
1334
&ron::ser::to_string(&NesterSerialized::Path("custom://bottom.nest".into())).unwrap(),
1335
);
1336
custom_source_dir
1337
.insert_asset_text(Path::new("bottom.nest"), &serialize_as_leaf("leaf".into()));
1338
default_source_dir.insert_asset_text(
1339
Path::new("unrelated.nest"),
1340
&serialize_as_leaf("unrelated".into()),
1341
);
1342
1343
run_app_until_finished_processing(&mut app, guard);
1344
1345
// The initial processing step should have processed all assets.
1346
assert_eq!(
1347
read_asset_as_string(&custom_processed_dir, Path::new("bottom.nest")),
1348
serialize_as_leaf("leaf-ref".into())
1349
);
1350
assert_eq!(
1351
read_asset_as_string(&default_processed_dir, Path::new("middle.nest")),
1352
serialize_as_leaf("leaf-ref-ref".into())
1353
);
1354
assert_eq!(
1355
read_asset_as_string(&custom_processed_dir, Path::new("top.nest")),
1356
serialize_as_leaf("leaf-ref-ref-ref".into())
1357
);
1358
assert_eq!(
1359
read_asset_as_string(&default_processed_dir, Path::new("unrelated.nest")),
1360
serialize_as_leaf("unrelated-ref".into())
1361
);
1362
1363
let get_process_count = || {
1364
*process_counter
1365
.lock()
1366
.unwrap_or_else(PoisonError::into_inner)
1367
};
1368
assert_eq!(get_process_count(), 4);
1369
1370
// Now we will only send a single source event, but that should still result in all related
1371
// assets being reprocessed.
1372
let guard = source_gate.write_blocking();
1373
1374
custom_source_dir.insert_asset_text(
1375
Path::new("bottom.nest"),
1376
&serialize_as_leaf("leaf changed".into()),
1377
);
1378
custom_source_events
1379
.send_blocking(AssetSourceEvent::ModifiedAsset("bottom.nest".into()))
1380
.unwrap();
1381
1382
run_app_until_finished_processing(&mut app, guard);
1383
1384
assert_eq!(
1385
read_asset_as_string(&custom_processed_dir, Path::new("bottom.nest")),
1386
serialize_as_leaf("leaf changed-ref".into())
1387
);
1388
assert_eq!(
1389
read_asset_as_string(&default_processed_dir, Path::new("middle.nest")),
1390
serialize_as_leaf("leaf changed-ref-ref".into())
1391
);
1392
assert_eq!(
1393
read_asset_as_string(&custom_processed_dir, Path::new("top.nest")),
1394
serialize_as_leaf("leaf changed-ref-ref-ref".into())
1395
);
1396
assert_eq!(
1397
read_asset_as_string(&default_processed_dir, Path::new("unrelated.nest")),
1398
serialize_as_leaf("unrelated-ref".into())
1399
);
1400
1401
assert_eq!(get_process_count(), 7);
1402
1403
// Send a modify event to the middle asset without changing the asset bytes. This should do
1404
// **nothing** since neither its dependencies nor its bytes have changed.
1405
let guard = source_gate.write_blocking();
1406
1407
default_source_events
1408
.send_blocking(AssetSourceEvent::ModifiedAsset("middle.nest".into()))
1409
.unwrap();
1410
1411
run_app_until_finished_processing(&mut app, guard);
1412
1413
assert_eq!(
1414
read_asset_as_string(&custom_processed_dir, Path::new("bottom.nest")),
1415
serialize_as_leaf("leaf changed-ref".into())
1416
);
1417
assert_eq!(
1418
read_asset_as_string(&default_processed_dir, Path::new("middle.nest")),
1419
serialize_as_leaf("leaf changed-ref-ref".into())
1420
);
1421
assert_eq!(
1422
read_asset_as_string(&custom_processed_dir, Path::new("top.nest")),
1423
serialize_as_leaf("leaf changed-ref-ref-ref".into())
1424
);
1425
assert_eq!(
1426
read_asset_as_string(&default_processed_dir, Path::new("unrelated.nest")),
1427
serialize_as_leaf("unrelated-ref".into())
1428
);
1429
1430
assert_eq!(get_process_count(), 7);
1431
}
1432
1433
#[test]
1434
fn clears_invalid_data_from_processed_dir() {
1435
let AppWithProcessor {
1436
mut app,
1437
source_gate,
1438
default_source_dirs:
1439
ProcessingDirs {
1440
source: default_source_dir,
1441
processed: default_processed_dir,
1442
..
1443
},
1444
..
1445
} = create_app_with_asset_processor(&[]);
1446
1447
type CoolTextProcessor = LoadTransformAndSave<
1448
CoolTextLoader,
1449
RootAssetTransformer<AddText, CoolText>,
1450
CoolTextSaver,
1451
>;
1452
app.init_asset::<CoolText>()
1453
.init_asset::<SubText>()
1454
.register_asset_loader(CoolTextLoader)
1455
.register_asset_processor(CoolTextProcessor::new(
1456
RootAssetTransformer::new(AddText(" processed".to_string())),
1457
CoolTextSaver,
1458
))
1459
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1460
1461
let guard = source_gate.write_blocking();
1462
1463
default_source_dir.insert_asset_text(Path::new("a.cool.ron"), &serialize_as_cool_text("a"));
1464
default_source_dir.insert_asset_text(Path::new("dir/b.cool.ron"), &serialize_as_cool_text("b"));
1465
default_source_dir.insert_asset_text(
1466
Path::new("dir/subdir/c.cool.ron"),
1467
&serialize_as_cool_text("c"),
1468
);
1469
1470
// This asset has the right data, but no meta, so it should be reprocessed.
1471
let a = Path::new("a.cool.ron");
1472
default_processed_dir.insert_asset_text(a, &serialize_as_cool_text("a processed"));
1473
// These assets aren't present in the unprocessed directory, so they should be deleted.
1474
let missing1 = Path::new("missing1.cool.ron");
1475
let missing2 = Path::new("dir/missing2.cool.ron");
1476
let missing3 = Path::new("other_dir/missing3.cool.ron");
1477
default_processed_dir.insert_asset_text(missing1, &serialize_as_cool_text("missing1"));
1478
default_processed_dir.insert_meta_text(missing1, ""); // This asset has metadata.
1479
default_processed_dir.insert_asset_text(missing2, &serialize_as_cool_text("missing2"));
1480
default_processed_dir.insert_asset_text(missing3, &serialize_as_cool_text("missing3"));
1481
// This directory is empty, so it should be deleted.
1482
let empty_dir = Path::new("empty_dir");
1483
let empty_dir_subdir = Path::new("empty_dir/empty_subdir");
1484
default_processed_dir.get_or_insert_dir(empty_dir_subdir);
1485
1486
run_app_until_finished_processing(&mut app, guard);
1487
1488
assert_eq!(
1489
read_asset_as_string(&default_processed_dir, a),
1490
serialize_as_cool_text("a processed")
1491
);
1492
assert!(default_processed_dir.get_metadata(a).is_some());
1493
1494
assert!(default_processed_dir.get_asset(missing1).is_none());
1495
assert!(default_processed_dir.get_metadata(missing1).is_none());
1496
assert!(default_processed_dir.get_asset(missing2).is_none());
1497
assert!(default_processed_dir.get_asset(missing3).is_none());
1498
1499
assert!(default_processed_dir.get_dir(empty_dir_subdir).is_none());
1500
assert!(default_processed_dir.get_dir(empty_dir).is_none());
1501
}
1502
1503
#[test]
1504
fn only_reprocesses_wrong_hash_on_startup() {
1505
let no_deps_asset = Path::new("no_deps.cool.ron");
1506
let source_changed_asset = Path::new("source_changed.cool.ron");
1507
let dep_unchanged_asset = Path::new("dep_unchanged.cool.ron");
1508
let dep_changed_asset = Path::new("dep_changed.cool.ron");
1509
let default_source_dir;
1510
let default_processed_dir;
1511
1512
#[derive(TypePath, Clone)]
1513
struct MergeEmbeddedAndAddText;
1514
1515
impl MutateAsset<CoolText> for MergeEmbeddedAndAddText {
1516
fn mutate(&self, asset: &mut CoolText) {
1517
asset.text.push_str(" processed");
1518
if asset.embedded.is_empty() {
1519
return;
1520
}
1521
asset.text.push(' ');
1522
asset.text.push_str(&asset.embedded);
1523
// Clear the embedded text so that saving doesn't break.
1524
asset.embedded.clear();
1525
}
1526
}
1527
1528
#[derive(TypePath, Clone)]
1529
struct Count<T>(Arc<Mutex<u32>>, T);
1530
1531
impl<A: Asset, T: MutateAsset<A>> MutateAsset<A> for Count<T> {
1532
fn mutate(&self, asset: &mut A) {
1533
*self.0.lock().unwrap_or_else(PoisonError::into_inner) += 1;
1534
self.1.mutate(asset);
1535
}
1536
}
1537
1538
let transformer = Count(Arc::new(Mutex::new(0)), MergeEmbeddedAndAddText);
1539
type CoolTextProcessor = LoadTransformAndSave<
1540
CoolTextLoader,
1541
RootAssetTransformer<Count<MergeEmbeddedAndAddText>, CoolText>,
1542
CoolTextSaver,
1543
>;
1544
1545
// Create a scope so that the app is completely gone afterwards (and we can see what happens
1546
// after reinitializing).
1547
{
1548
let AppWithProcessor {
1549
mut app,
1550
source_gate,
1551
default_source_dirs,
1552
..
1553
} = create_app_with_asset_processor(&[]);
1554
default_source_dir = default_source_dirs.source;
1555
default_processed_dir = default_source_dirs.processed;
1556
1557
app.init_asset::<CoolText>()
1558
.init_asset::<SubText>()
1559
.register_asset_loader(CoolTextLoader)
1560
.register_asset_processor(CoolTextProcessor::new(
1561
RootAssetTransformer::new(transformer.clone()),
1562
CoolTextSaver,
1563
))
1564
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1565
1566
let guard = source_gate.write_blocking();
1567
1568
let cool_text_with_embedded = |text: &str, embedded: &Path| {
1569
let cool_text_ron = CoolTextRon {
1570
text: text.into(),
1571
dependencies: vec![],
1572
embedded_dependencies: vec![embedded.to_string_lossy().into_owned()],
1573
sub_texts: vec![],
1574
};
1575
ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
1576
};
1577
1578
default_source_dir.insert_asset_text(no_deps_asset, &serialize_as_cool_text("no_deps"));
1579
default_source_dir.insert_asset_text(
1580
source_changed_asset,
1581
&serialize_as_cool_text("source_changed"),
1582
);
1583
default_source_dir.insert_asset_text(
1584
dep_unchanged_asset,
1585
&cool_text_with_embedded("dep_unchanged", no_deps_asset),
1586
);
1587
default_source_dir.insert_asset_text(
1588
dep_changed_asset,
1589
&cool_text_with_embedded("dep_changed", source_changed_asset),
1590
);
1591
1592
run_app_until_finished_processing(&mut app, guard);
1593
1594
assert_eq!(
1595
read_asset_as_string(&default_processed_dir, no_deps_asset),
1596
serialize_as_cool_text("no_deps processed")
1597
);
1598
assert_eq!(
1599
read_asset_as_string(&default_processed_dir, source_changed_asset),
1600
serialize_as_cool_text("source_changed processed")
1601
);
1602
assert_eq!(
1603
read_asset_as_string(&default_processed_dir, dep_unchanged_asset),
1604
serialize_as_cool_text("dep_unchanged processed no_deps processed")
1605
);
1606
assert_eq!(
1607
read_asset_as_string(&default_processed_dir, dep_changed_asset),
1608
serialize_as_cool_text("dep_changed processed source_changed processed")
1609
);
1610
}
1611
1612
// Assert and reset the processing count.
1613
assert_eq!(
1614
core::mem::take(&mut *transformer.0.lock().unwrap_or_else(PoisonError::into_inner)),
1615
4
1616
);
1617
1618
// Hand-make the app, since we need to pass in our already existing Dirs from the last app.
1619
let mut app = App::new();
1620
let source_gate = Arc::new(RwLock::new(()));
1621
1622
let source_memory_reader = LockGatedReader::new(
1623
source_gate.clone(),
1624
MemoryAssetReader {
1625
root: default_source_dir.clone(),
1626
},
1627
);
1628
let processed_memory_reader = MemoryAssetReader {
1629
root: default_processed_dir.clone(),
1630
};
1631
let processed_memory_writer = MemoryAssetWriter {
1632
root: default_processed_dir.clone(),
1633
};
1634
1635
app.register_asset_source(
1636
AssetSourceId::Default,
1637
AssetSourceBuilder::new(move || Box::new(source_memory_reader.clone()))
1638
.with_processed_reader(move || Box::new(processed_memory_reader.clone()))
1639
.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),
1640
);
1641
1642
app.add_plugins((
1643
TaskPoolPlugin::default(),
1644
AssetPlugin {
1645
mode: AssetMode::Processed,
1646
use_asset_processor_override: Some(true),
1647
watch_for_changes_override: Some(true),
1648
..Default::default()
1649
},
1650
));
1651
1652
set_fake_transaction_log(&mut app);
1653
1654
app.init_asset::<CoolText>()
1655
.init_asset::<SubText>()
1656
.register_asset_loader(CoolTextLoader)
1657
.register_asset_processor(CoolTextProcessor::new(
1658
RootAssetTransformer::new(transformer.clone()),
1659
CoolTextSaver,
1660
))
1661
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1662
1663
let guard = source_gate.write_blocking();
1664
1665
default_source_dir
1666
.insert_asset_text(source_changed_asset, &serialize_as_cool_text("DIFFERENT"));
1667
1668
run_app_until_finished_processing(&mut app, guard);
1669
1670
// Only source_changed and dep_changed assets were reprocessed - all others still have the same
1671
// hashes.
1672
let num_processes = *transformer.0.lock().unwrap_or_else(PoisonError::into_inner);
1673
// TODO: assert_eq! (num_processes == 2) only after we prevent double processing assets
1674
// == 3 happens when the initial processing of an asset and the re-processing that its dependency
1675
// triggers are both able to proceed. (dep_changed_asset in this case is processed twice)
1676
assert!(num_processes == 2 || num_processes == 3);
1677
1678
assert_eq!(
1679
read_asset_as_string(&default_processed_dir, no_deps_asset),
1680
serialize_as_cool_text("no_deps processed")
1681
);
1682
assert_eq!(
1683
read_asset_as_string(&default_processed_dir, source_changed_asset),
1684
serialize_as_cool_text("DIFFERENT processed")
1685
);
1686
assert_eq!(
1687
read_asset_as_string(&default_processed_dir, dep_unchanged_asset),
1688
serialize_as_cool_text("dep_unchanged processed no_deps processed")
1689
);
1690
assert_eq!(
1691
read_asset_as_string(&default_processed_dir, dep_changed_asset),
1692
serialize_as_cool_text("dep_changed processed DIFFERENT processed")
1693
);
1694
}
1695
1696
#[test]
1697
fn writes_default_meta_for_processor() {
1698
let AppWithProcessor {
1699
mut app,
1700
default_source_dirs: ProcessingDirs { source, .. },
1701
..
1702
} = create_app_with_asset_processor(&[]);
1703
1704
type CoolTextProcessor = LoadTransformAndSave<
1705
CoolTextLoader,
1706
RootAssetTransformer<AddText, CoolText>,
1707
CoolTextSaver,
1708
>;
1709
1710
app.register_asset_processor(CoolTextProcessor::new(
1711
RootAssetTransformer::new(AddText("blah".to_string())),
1712
CoolTextSaver,
1713
))
1714
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1715
1716
const ASSET_PATH: &str = "abc.cool.ron";
1717
source.insert_asset_text(Path::new(ASSET_PATH), &serialize_as_cool_text("blah"));
1718
1719
let processor = app.world().resource::<AssetProcessor>().clone();
1720
bevy_tasks::block_on(processor.write_default_meta_file_for_path(ASSET_PATH)).unwrap();
1721
1722
assert_eq!(
1723
read_meta_as_string(&source, Path::new(ASSET_PATH)),
1724
r#"(
1725
meta_format_version: "1.0",
1726
asset: Process(
1727
processor: "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>",
1728
settings: (
1729
loader_settings: (),
1730
transformer_settings: (),
1731
saver_settings: (),
1732
),
1733
),
1734
)"#
1735
);
1736
}
1737
1738
#[test]
1739
fn write_default_meta_does_not_overwrite() {
1740
let AppWithProcessor {
1741
mut app,
1742
default_source_dirs: ProcessingDirs { source, .. },
1743
..
1744
} = create_app_with_asset_processor(&[]);
1745
1746
type CoolTextProcessor = LoadTransformAndSave<
1747
CoolTextLoader,
1748
RootAssetTransformer<AddText, CoolText>,
1749
CoolTextSaver,
1750
>;
1751
1752
app.register_asset_processor(CoolTextProcessor::new(
1753
RootAssetTransformer::new(AddText("blah".to_string())),
1754
CoolTextSaver,
1755
))
1756
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1757
1758
const ASSET_PATH: &str = "abc.cool.ron";
1759
source.insert_asset_text(Path::new(ASSET_PATH), &serialize_as_cool_text("blah"));
1760
const META_TEXT: &str = "hey i'm walkin here!";
1761
source.insert_meta_text(Path::new(ASSET_PATH), META_TEXT);
1762
1763
let processor = app.world().resource::<AssetProcessor>().clone();
1764
assert!(matches!(
1765
bevy_tasks::block_on(processor.write_default_meta_file_for_path(ASSET_PATH)),
1766
Err(WriteDefaultMetaError::MetaAlreadyExists)
1767
));
1768
1769
assert_eq!(
1770
read_meta_as_string(&source, Path::new(ASSET_PATH)),
1771
META_TEXT
1772
);
1773
}
1774
1775