Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_asset/src/processor/tests.rs
7223 views
1
use alloc::{
2
boxed::Box,
3
collections::BTreeMap,
4
string::{String, ToString},
5
vec,
6
vec::Vec,
7
};
8
use bevy_reflect::TypePath;
9
use core::marker::PhantomData;
10
use futures_lite::AsyncWriteExt;
11
use serde::{Deserialize, Serialize};
12
use std::path::Path;
13
14
use bevy_app::{App, TaskPoolPlugin};
15
use bevy_ecs::error::BevyError;
16
use bevy_tasks::BoxedFuture;
17
18
use crate::{
19
io::{
20
memory::{Dir, MemoryAssetReader, MemoryAssetWriter},
21
AssetSource, AssetSourceId, Reader,
22
},
23
processor::{
24
AssetProcessor, LoadTransformAndSave, LogEntry, ProcessorState, ProcessorTransactionLog,
25
ProcessorTransactionLogFactory,
26
},
27
saver::AssetSaver,
28
tests::{run_app_until, CoolText, CoolTextLoader, CoolTextRon, SubText},
29
transformer::{AssetTransformer, TransformedAsset},
30
Asset, AssetApp, AssetLoader, AssetMode, AssetPath, AssetPlugin, LoadContext,
31
};
32
33
struct AppWithProcessor {
34
app: App,
35
source_dir: Dir,
36
processed_dir: Dir,
37
}
38
39
fn create_app_with_asset_processor() -> AppWithProcessor {
40
let mut app = App::new();
41
let source_dir = Dir::default();
42
let processed_dir = Dir::default();
43
44
let source_memory_reader = MemoryAssetReader {
45
root: source_dir.clone(),
46
};
47
let processed_memory_reader = MemoryAssetReader {
48
root: processed_dir.clone(),
49
};
50
let processed_memory_writer = MemoryAssetWriter {
51
root: processed_dir.clone(),
52
};
53
54
app.register_asset_source(
55
AssetSourceId::Default,
56
AssetSource::build()
57
.with_reader(move || Box::new(source_memory_reader.clone()))
58
.with_processed_reader(move || Box::new(processed_memory_reader.clone()))
59
.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),
60
)
61
.add_plugins((
62
TaskPoolPlugin::default(),
63
AssetPlugin {
64
mode: AssetMode::Processed,
65
use_asset_processor_override: Some(true),
66
..Default::default()
67
},
68
));
69
70
/// A dummy transaction log factory that just creates [`FakeTransactionLog`].
71
struct FakeTransactionLogFactory;
72
73
impl ProcessorTransactionLogFactory for FakeTransactionLogFactory {
74
fn read(&self) -> BoxedFuture<'_, Result<Vec<LogEntry>, BevyError>> {
75
Box::pin(async move { Ok(vec![]) })
76
}
77
78
fn create_new_log(
79
&self,
80
) -> BoxedFuture<'_, Result<Box<dyn ProcessorTransactionLog>, BevyError>> {
81
Box::pin(async move { Ok(Box::new(FakeTransactionLog) as _) })
82
}
83
}
84
85
/// A dummy transaction log that just drops every log.
86
// TODO: In the future it's possible for us to have a test of the transaction log, so making
87
// this more complex may be necessary.
88
struct FakeTransactionLog;
89
90
impl ProcessorTransactionLog for FakeTransactionLog {
91
fn begin_processing<'a>(
92
&'a mut self,
93
_asset: &'a AssetPath<'_>,
94
) -> BoxedFuture<'a, Result<(), BevyError>> {
95
Box::pin(async move { Ok(()) })
96
}
97
98
fn end_processing<'a>(
99
&'a mut self,
100
_asset: &'a AssetPath<'_>,
101
) -> BoxedFuture<'a, Result<(), BevyError>> {
102
Box::pin(async move { Ok(()) })
103
}
104
105
fn unrecoverable(&mut self) -> BoxedFuture<'_, Result<(), BevyError>> {
106
Box::pin(async move { Ok(()) })
107
}
108
}
109
110
app.world()
111
.resource::<AssetProcessor>()
112
.data()
113
.set_log_factory(Box::new(FakeTransactionLogFactory))
114
.unwrap();
115
116
AppWithProcessor {
117
app,
118
source_dir,
119
processed_dir,
120
}
121
}
122
123
fn run_app_until_finished_processing(app: &mut App) {
124
run_app_until(app, |world| {
125
if bevy_tasks::block_on(world.resource::<AssetProcessor>().get_state())
126
== ProcessorState::Finished
127
{
128
Some(())
129
} else {
130
None
131
}
132
});
133
}
134
135
struct CoolTextSaver;
136
137
impl AssetSaver for CoolTextSaver {
138
type Asset = CoolText;
139
type Settings = ();
140
type OutputLoader = CoolTextLoader;
141
type Error = std::io::Error;
142
143
async fn save(
144
&self,
145
writer: &mut crate::io::Writer,
146
asset: crate::saver::SavedAsset<'_, Self::Asset>,
147
_: &Self::Settings,
148
) -> Result<(), Self::Error> {
149
let ron = CoolTextRon {
150
text: asset.text.clone(),
151
sub_texts: asset
152
.iter_labels()
153
.map(|label| asset.get_labeled::<SubText, _>(label).unwrap().text.clone())
154
.collect(),
155
dependencies: asset
156
.dependencies
157
.iter()
158
.map(|handle| handle.path().unwrap().path())
159
.map(|path| path.to_str().unwrap().to_string())
160
.collect(),
161
// NOTE: We can't handle embedded dependencies in any way, since we need to write to
162
// another file to do so.
163
embedded_dependencies: vec![],
164
};
165
let ron = ron::ser::to_string(&ron).unwrap();
166
writer.write_all(ron.as_bytes()).await?;
167
Ok(())
168
}
169
}
170
171
// Note: while we allow any Fn, since closures are unnameable types, creating a processor with a
172
// closure cannot be used (since we need to include the name of the transformer in the meta
173
// file).
174
struct RootAssetTransformer<M: MutateAsset<A>, A: Asset>(M, PhantomData<fn(&mut A)>);
175
176
trait MutateAsset<A: Asset>: Send + Sync + 'static {
177
fn mutate(&self, asset: &mut A);
178
}
179
180
impl<M: MutateAsset<A>, A: Asset> RootAssetTransformer<M, A> {
181
fn new(m: M) -> Self {
182
Self(m, PhantomData)
183
}
184
}
185
186
impl<M: MutateAsset<A>, A: Asset> AssetTransformer for RootAssetTransformer<M, A> {
187
type AssetInput = A;
188
type AssetOutput = A;
189
type Error = std::io::Error;
190
type Settings = ();
191
192
async fn transform<'a>(
193
&'a self,
194
mut asset: TransformedAsset<A>,
195
_settings: &'a Self::Settings,
196
) -> Result<TransformedAsset<A>, Self::Error> {
197
self.0.mutate(asset.get_mut());
198
Ok(asset)
199
}
200
}
201
202
#[test]
203
fn no_meta_or_default_processor_copies_asset() {
204
// Assets without a meta file or a default processor should still be accessible in the
205
// processed path. Note: This isn't exactly the desired property - we don't want the assets
206
// to be copied to the processed directory. We just want these assets to still be loadable
207
// if we no longer have the source directory. This could be done with a symlink instead of a
208
// copy.
209
210
let AppWithProcessor {
211
mut app,
212
source_dir,
213
processed_dir,
214
} = create_app_with_asset_processor();
215
216
let path = Path::new("abc.cool.ron");
217
let source_asset = r#"(
218
text: "abc",
219
dependencies: [],
220
embedded_dependencies: [],
221
sub_texts: [],
222
)"#;
223
224
source_dir.insert_asset_text(path, source_asset);
225
226
run_app_until_finished_processing(&mut app);
227
228
let processed_asset = processed_dir.get_asset(path).unwrap();
229
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
230
assert_eq!(processed_asset, source_asset);
231
}
232
233
#[test]
234
fn asset_processor_transforms_asset_default_processor() {
235
let AppWithProcessor {
236
mut app,
237
source_dir,
238
processed_dir,
239
} = create_app_with_asset_processor();
240
241
struct AddText;
242
243
impl MutateAsset<CoolText> for AddText {
244
fn mutate(&self, text: &mut CoolText) {
245
text.text.push_str("_def");
246
}
247
}
248
249
type CoolTextProcessor = LoadTransformAndSave<
250
CoolTextLoader,
251
RootAssetTransformer<AddText, CoolText>,
252
CoolTextSaver,
253
>;
254
app.register_asset_loader(CoolTextLoader)
255
.register_asset_processor(CoolTextProcessor::new(
256
RootAssetTransformer::new(AddText),
257
CoolTextSaver,
258
))
259
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
260
261
let path = Path::new("abc.cool.ron");
262
source_dir.insert_asset_text(
263
path,
264
r#"(
265
text: "abc",
266
dependencies: [],
267
embedded_dependencies: [],
268
sub_texts: [],
269
)"#,
270
);
271
272
run_app_until_finished_processing(&mut app);
273
274
let processed_asset = processed_dir.get_asset(path).unwrap();
275
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
276
assert_eq!(
277
processed_asset,
278
r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#
279
);
280
}
281
282
#[test]
283
fn asset_processor_transforms_asset_with_meta() {
284
let AppWithProcessor {
285
mut app,
286
source_dir,
287
processed_dir,
288
} = create_app_with_asset_processor();
289
290
struct AddText;
291
292
impl MutateAsset<CoolText> for AddText {
293
fn mutate(&self, text: &mut CoolText) {
294
text.text.push_str("_def");
295
}
296
}
297
298
type CoolTextProcessor = LoadTransformAndSave<
299
CoolTextLoader,
300
RootAssetTransformer<AddText, CoolText>,
301
CoolTextSaver,
302
>;
303
app.register_asset_loader(CoolTextLoader)
304
.register_asset_processor(CoolTextProcessor::new(
305
RootAssetTransformer::new(AddText),
306
CoolTextSaver,
307
));
308
309
let path = Path::new("abc.cool.ron");
310
source_dir.insert_asset_text(
311
path,
312
r#"(
313
text: "abc",
314
dependencies: [],
315
embedded_dependencies: [],
316
sub_texts: [],
317
)"#,
318
);
319
source_dir.insert_meta_text(path, r#"(
320
meta_format_version: "1.0",
321
asset: Process(
322
processor: "bevy_asset::processor::process::LoadTransformAndSave<bevy_asset::tests::CoolTextLoader, bevy_asset::processor::tests::RootAssetTransformer<bevy_asset::processor::tests::asset_processor_transforms_asset_with_meta::AddText, bevy_asset::tests::CoolText>, bevy_asset::processor::tests::CoolTextSaver>",
323
settings: (
324
loader_settings: (),
325
transformer_settings: (),
326
saver_settings: (),
327
),
328
),
329
)"#);
330
331
run_app_until_finished_processing(&mut app);
332
333
let processed_asset = processed_dir.get_asset(path).unwrap();
334
let processed_asset = str::from_utf8(processed_asset.value()).unwrap();
335
assert_eq!(
336
processed_asset,
337
r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"#
338
);
339
}
340
341
#[derive(Asset, TypePath, Serialize, Deserialize)]
342
struct FakeGltf {
343
gltf_nodes: BTreeMap<String, String>,
344
}
345
346
struct FakeGltfLoader;
347
348
impl AssetLoader for FakeGltfLoader {
349
type Asset = FakeGltf;
350
type Settings = ();
351
type Error = std::io::Error;
352
353
async fn load(
354
&self,
355
reader: &mut dyn Reader,
356
_settings: &Self::Settings,
357
_load_context: &mut LoadContext<'_>,
358
) -> Result<Self::Asset, Self::Error> {
359
use std::io::{Error, ErrorKind};
360
361
let mut bytes = vec![];
362
reader.read_to_end(&mut bytes).await?;
363
ron::de::from_bytes(&bytes)
364
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))
365
}
366
367
fn extensions(&self) -> &[&str] {
368
&["gltf"]
369
}
370
}
371
372
#[derive(Asset, TypePath, Serialize, Deserialize)]
373
struct FakeBsn {
374
parent_bsn: Option<String>,
375
nodes: BTreeMap<String, String>,
376
}
377
378
// This loader loads the BSN but as an "inlined" scene. We read the original BSN and create a
379
// scene that holds all the data including parents.
380
// TODO: It would be nice if the inlining was actually done as an `AssetTransformer`, but
381
// `Process` currently has no way to load nested assets.
382
struct FakeBsnLoader;
383
384
impl AssetLoader for FakeBsnLoader {
385
type Asset = FakeBsn;
386
type Settings = ();
387
type Error = std::io::Error;
388
389
async fn load(
390
&self,
391
reader: &mut dyn Reader,
392
_settings: &Self::Settings,
393
load_context: &mut LoadContext<'_>,
394
) -> Result<Self::Asset, Self::Error> {
395
use std::io::{Error, ErrorKind};
396
397
let mut bytes = vec![];
398
reader.read_to_end(&mut bytes).await?;
399
let bsn: FakeBsn = ron::de::from_bytes(&bytes)
400
.map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?;
401
402
if bsn.parent_bsn.is_none() {
403
return Ok(bsn);
404
}
405
406
let parent_bsn = bsn.parent_bsn.unwrap();
407
let parent_bsn = load_context
408
.loader()
409
.immediate()
410
.load(parent_bsn)
411
.await
412
.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
413
let mut new_bsn: FakeBsn = parent_bsn.take();
414
for (name, node) in bsn.nodes {
415
new_bsn.nodes.insert(name, node);
416
}
417
Ok(new_bsn)
418
}
419
420
fn extensions(&self) -> &[&str] {
421
&["bsn"]
422
}
423
}
424
425
#[derive(TypePath)]
426
struct GltfToBsn;
427
428
impl AssetTransformer for GltfToBsn {
429
type AssetInput = FakeGltf;
430
type AssetOutput = FakeBsn;
431
type Settings = ();
432
type Error = std::io::Error;
433
434
async fn transform<'a>(
435
&'a self,
436
mut asset: TransformedAsset<Self::AssetInput>,
437
_settings: &'a Self::Settings,
438
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
439
let bsn = FakeBsn {
440
parent_bsn: None,
441
// Pretend we converted all the glTF nodes into BSN's format.
442
nodes: core::mem::take(&mut asset.get_mut().gltf_nodes),
443
};
444
Ok(asset.replace_asset(bsn))
445
}
446
}
447
448
#[derive(TypePath)]
449
struct FakeBsnSaver;
450
451
impl AssetSaver for FakeBsnSaver {
452
type Asset = FakeBsn;
453
type Error = std::io::Error;
454
type OutputLoader = FakeBsnLoader;
455
type Settings = ();
456
457
async fn save(
458
&self,
459
writer: &mut crate::io::Writer,
460
asset: crate::saver::SavedAsset<'_, Self::Asset>,
461
_settings: &Self::Settings,
462
) -> Result<(), Self::Error> {
463
use std::io::{Error, ErrorKind};
464
465
use ron::ser::PrettyConfig;
466
467
let ron_string =
468
ron::ser::to_string_pretty(asset.get(), PrettyConfig::new().new_line("\n"))
469
.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
470
471
writer.write_all(ron_string.as_bytes()).await
472
}
473
}
474
#[test]
475
fn asset_processor_loading_can_read_processed_assets() {
476
use crate::transformer::IdentityAssetTransformer;
477
478
let AppWithProcessor {
479
mut app,
480
source_dir,
481
processed_dir,
482
} = create_app_with_asset_processor();
483
484
// This processor loads a gltf file, converts it to BSN and then saves out the BSN.
485
type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;
486
// This processor loads a BSN file (which "inlines" parent BSNs at load), and then saves the
487
// inlined BSN.
488
type BsnProcessor =
489
LoadTransformAndSave<FakeBsnLoader, IdentityAssetTransformer<FakeBsn>, FakeBsnSaver>;
490
app.register_asset_loader(FakeBsnLoader)
491
.register_asset_loader(FakeGltfLoader)
492
.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))
493
.register_asset_processor(BsnProcessor::new(
494
IdentityAssetTransformer::new(),
495
FakeBsnSaver,
496
))
497
.set_default_asset_processor::<GltfProcessor>("gltf")
498
.set_default_asset_processor::<BsnProcessor>("bsn");
499
500
let gltf_path = Path::new("abc.gltf");
501
source_dir.insert_asset_text(
502
gltf_path,
503
r#"(
504
gltf_nodes: {
505
"name": "thing",
506
"position": "123",
507
}
508
)"#,
509
);
510
let bsn_path = Path::new("def.bsn");
511
// The bsn tries to load the gltf as a bsn. This only works if the bsn can read processed
512
// assets.
513
source_dir.insert_asset_text(
514
bsn_path,
515
r#"(
516
parent_bsn: Some("abc.gltf"),
517
nodes: {
518
"position": "456",
519
"color": "red",
520
},
521
)"#,
522
);
523
524
run_app_until_finished_processing(&mut app);
525
526
let processed_bsn = processed_dir.get_asset(bsn_path).unwrap();
527
let processed_bsn = str::from_utf8(processed_bsn.value()).unwrap();
528
// The processed bsn should have been "inlined", so no parent and "overlaid" nodes.
529
assert_eq!(
530
processed_bsn,
531
r#"(
532
parent_bsn: None,
533
nodes: {
534
"color": "red",
535
"name": "thing",
536
"position": "456",
537
},
538
)"#
539
);
540
}
541
542
#[test]
543
fn asset_processor_loading_can_read_source_assets() {
544
let AppWithProcessor {
545
mut app,
546
source_dir,
547
processed_dir,
548
} = create_app_with_asset_processor();
549
550
#[derive(Serialize, Deserialize)]
551
struct FakeGltfxData {
552
// These are the file paths to the gltfs.
553
gltfs: Vec<String>,
554
}
555
556
#[derive(Asset, TypePath)]
557
struct FakeGltfx {
558
gltfs: Vec<FakeGltf>,
559
}
560
561
#[derive(TypePath)]
562
struct FakeGltfxLoader;
563
564
impl AssetLoader for FakeGltfxLoader {
565
type Asset = FakeGltfx;
566
type Error = std::io::Error;
567
type Settings = ();
568
569
async fn load(
570
&self,
571
reader: &mut dyn Reader,
572
_settings: &Self::Settings,
573
load_context: &mut LoadContext<'_>,
574
) -> Result<Self::Asset, Self::Error> {
575
use std::io::{Error, ErrorKind};
576
577
let mut buf = vec![];
578
reader.read_to_end(&mut buf).await?;
579
580
let gltfx_data: FakeGltfxData =
581
ron::de::from_bytes(&buf).map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
582
583
let mut gltfs = vec![];
584
for gltf in gltfx_data.gltfs.into_iter() {
585
// gltfx files come from "generic" software that doesn't know anything about
586
// Bevy, so it needs to load the source assets to make sense.
587
let gltf = load_context
588
.loader()
589
.immediate()
590
.load(gltf)
591
.await
592
.map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
593
gltfs.push(gltf.take());
594
}
595
596
Ok(FakeGltfx { gltfs })
597
}
598
599
fn extensions(&self) -> &[&str] {
600
&["gltfx"]
601
}
602
}
603
604
#[derive(TypePath)]
605
struct GltfxToBsn;
606
607
impl AssetTransformer for GltfxToBsn {
608
type AssetInput = FakeGltfx;
609
type AssetOutput = FakeBsn;
610
type Settings = ();
611
type Error = std::io::Error;
612
613
async fn transform<'a>(
614
&'a self,
615
mut asset: TransformedAsset<Self::AssetInput>,
616
_settings: &'a Self::Settings,
617
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
618
let gltfx = asset.get_mut();
619
620
// Merge together all the gltfs from the gltfx into one big bsn.
621
let bsn = gltfx.gltfs.drain(..).fold(
622
FakeBsn {
623
parent_bsn: None,
624
nodes: Default::default(),
625
},
626
|mut bsn, gltf| {
627
for (key, value) in gltf.gltf_nodes {
628
bsn.nodes.insert(key, value);
629
}
630
bsn
631
},
632
);
633
634
Ok(asset.replace_asset(bsn))
635
}
636
}
637
638
// This processor loads a gltf file, converts it to BSN and then saves out the BSN.
639
type GltfProcessor = LoadTransformAndSave<FakeGltfLoader, GltfToBsn, FakeBsnSaver>;
640
// This processor loads a gltfx file (including its gltf files) and converts it to BSN.
641
type GltfxProcessor = LoadTransformAndSave<FakeGltfxLoader, GltfxToBsn, FakeBsnSaver>;
642
app.register_asset_loader(FakeGltfLoader)
643
.register_asset_loader(FakeGltfxLoader)
644
.register_asset_loader(FakeBsnLoader)
645
.register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver))
646
.register_asset_processor(GltfxProcessor::new(GltfxToBsn, FakeBsnSaver))
647
.set_default_asset_processor::<GltfProcessor>("gltf")
648
.set_default_asset_processor::<GltfxProcessor>("gltfx");
649
650
let gltf_path_1 = Path::new("abc.gltf");
651
source_dir.insert_asset_text(
652
gltf_path_1,
653
r#"(
654
gltf_nodes: {
655
"name": "thing",
656
"position": "123",
657
}
658
)"#,
659
);
660
let gltf_path_2 = Path::new("def.gltf");
661
source_dir.insert_asset_text(
662
gltf_path_2,
663
r#"(
664
gltf_nodes: {
665
"velocity": "456",
666
"color": "red",
667
}
668
)"#,
669
);
670
671
let gltfx_path = Path::new("xyz.gltfx");
672
source_dir.insert_asset_text(
673
gltfx_path,
674
r#"(
675
gltfs: ["abc.gltf", "def.gltf"],
676
)"#,
677
);
678
679
run_app_until_finished_processing(&mut app);
680
681
// Sanity check that the two gltf files were actually processed.
682
let processed_gltf_1 = processed_dir.get_asset(gltf_path_1).unwrap();
683
let processed_gltf_1 = str::from_utf8(processed_gltf_1.value()).unwrap();
684
assert_eq!(
685
processed_gltf_1,
686
r#"(
687
parent_bsn: None,
688
nodes: {
689
"name": "thing",
690
"position": "123",
691
},
692
)"#
693
);
694
let processed_gltf_2 = processed_dir.get_asset(gltf_path_2).unwrap();
695
let processed_gltf_2 = str::from_utf8(processed_gltf_2.value()).unwrap();
696
assert_eq!(
697
processed_gltf_2,
698
r#"(
699
parent_bsn: None,
700
nodes: {
701
"color": "red",
702
"velocity": "456",
703
},
704
)"#
705
);
706
707
// The processed gltfx should have been able to load and merge the gltfs despite them having
708
// been processed into bsn.
709
710
// Blocked on https://github.com/bevyengine/bevy/issues/21269. This is the actual assertion.
711
// let processed_gltfx = processed_dir.get_asset(gltfx_path).unwrap();
712
// let processed_gltfx = str::from_utf8(processed_gltfx.value()).unwrap();
713
// assert_eq!(
714
// processed_gltfx,
715
// r#"(
716
// parent_bsn: None,
717
// nodes: {
718
// "color": "red",
719
// "name": "thing",
720
// "position": "123",
721
// "velocity": "456",
722
// },
723
// )"#
724
// );
725
726
// This assertion exists to "prove" that this problem exists.
727
assert!(processed_dir.get_asset(gltfx_path).is_none());
728
}
729
730