Skip to content

Commit 2146899

Browse files
authored
Add a few basic tests for asset processing. (#21409)
# Objective - `bevy_asset` needs more tests! This adds three related to asset processing. ## Solution - Create a new `MemoryAssetWriter` to pair with `MemoryAssetReader`. - Adds a way to override whether the asset processor is created or not. - Make `Data::value` and `Data::path` `pub` so that we can actually see what is written to the processed dir. Note: `Dir::get_asset` returns `Data` already, but it isn't usable. - Add three tests: one to test that assets are copied to the processed dir for no-processing assets, one to test that using a default processor works, and one to test that an asset meta file works. ## Testing - This adds testing! :D
1 parent 1610aa9 commit 2146899

File tree

4 files changed

+579
-26
lines changed

4 files changed

+579
-26
lines changed

crates/bevy_asset/src/io/memory.rs

Lines changed: 234 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
use crate::io::{AssetReader, AssetReaderError, PathStream, Reader};
2-
use alloc::{borrow::ToOwned, boxed::Box, sync::Arc, vec::Vec};
1+
use crate::io::{AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, Reader};
2+
use alloc::{borrow::ToOwned, boxed::Box, sync::Arc, vec, vec::Vec};
33
use bevy_platform::{
44
collections::HashMap,
55
sync::{PoisonError, RwLock},
66
};
77
use core::{pin::Pin, task::Poll};
8-
use futures_io::AsyncRead;
8+
use futures_io::{AsyncRead, AsyncWrite};
99
use futures_lite::{ready, Stream};
10-
use std::path::{Path, PathBuf};
10+
use std::{
11+
io::{Error, ErrorKind},
12+
path::{Path, PathBuf},
13+
};
1114

1215
use super::AsyncSeekForward;
1316

@@ -59,7 +62,9 @@ impl Dir {
5962
);
6063
}
6164

62-
/// Removes the stored asset at `path` and returns the `Data` stored if found and otherwise `None`.
65+
/// Removes the stored asset at `path`.
66+
///
67+
/// Returns the [`Data`] stored if found, [`None`] otherwise.
6368
pub fn remove_asset(&self, path: &Path) -> Option<Data> {
6469
let mut dir = self.clone();
6570
if let Some(parent) = path.parent() {
@@ -91,6 +96,22 @@ impl Dir {
9196
);
9297
}
9398

99+
/// Removes the stored metadata at `path`.
100+
///
101+
/// Returns the [`Data`] stored if found, [`None`] otherwise.
102+
pub fn remove_metadata(&self, path: &Path) -> Option<Data> {
103+
let mut dir = self.clone();
104+
if let Some(parent) = path.parent() {
105+
dir = self.get_or_insert_dir(parent);
106+
}
107+
let key: Box<str> = path.file_name().unwrap().to_string_lossy().into();
108+
dir.0
109+
.write()
110+
.unwrap_or_else(PoisonError::into_inner)
111+
.metadata
112+
.remove(&key)
113+
}
114+
94115
pub fn get_or_insert_dir(&self, path: &Path) -> Dir {
95116
let mut dir = self.clone();
96117
let mut full_path = PathBuf::new();
@@ -108,6 +129,22 @@ impl Dir {
108129
dir
109130
}
110131

132+
/// Removes the dir at `path`.
133+
///
134+
/// Returns the [`Dir`] stored if found, [`None`] otherwise.
135+
pub fn remove_dir(&self, path: &Path) -> Option<Dir> {
136+
let mut dir = self.clone();
137+
if let Some(parent) = path.parent() {
138+
dir = self.get_or_insert_dir(parent);
139+
}
140+
let key: Box<str> = path.file_name().unwrap().to_string_lossy().into();
141+
dir.0
142+
.write()
143+
.unwrap_or_else(PoisonError::into_inner)
144+
.dirs
145+
.remove(&key)
146+
}
147+
111148
pub fn get_dir(&self, path: &Path) -> Option<Dir> {
112149
let mut dir = self.clone();
113150
for p in path.components() {
@@ -215,6 +252,14 @@ pub struct MemoryAssetReader {
215252
pub root: Dir,
216253
}
217254

255+
/// In-memory [`AssetWriter`] implementation.
256+
///
257+
/// This is primarily intended for unit tests.
258+
#[derive(Default, Clone)]
259+
pub struct MemoryAssetWriter {
260+
pub root: Dir,
261+
}
262+
218263
/// Asset data stored in a [`Dir`].
219264
#[derive(Clone, Debug)]
220265
pub struct Data {
@@ -230,10 +275,13 @@ pub enum Value {
230275
}
231276

232277
impl Data {
233-
fn path(&self) -> &Path {
278+
/// The path that this data was written to.
279+
pub fn path(&self) -> &Path {
234280
&self.path
235281
}
236-
fn value(&self) -> &[u8] {
282+
283+
/// The value in bytes that was written here.
284+
pub fn value(&self) -> &[u8] {
237285
match &self.value {
238286
Value::Vec(vec) => vec,
239287
Value::Static(value) => value,
@@ -296,8 +344,8 @@ impl AsyncSeekForward for DataReader {
296344
self.bytes_read = new_pos as _;
297345
Poll::Ready(Ok(new_pos as _))
298346
} else {
299-
Poll::Ready(Err(std::io::Error::new(
300-
std::io::ErrorKind::InvalidInput,
347+
Poll::Ready(Err(Error::new(
348+
ErrorKind::InvalidInput,
301349
"seek position is out of range",
302350
)))
303351
}
@@ -361,6 +409,183 @@ impl AssetReader for MemoryAssetReader {
361409
}
362410
}
363411

412+
/// A writer that writes into [`Dir`], buffering internally until flushed/closed.
413+
struct DataWriter {
414+
/// The dir to write to.
415+
dir: Dir,
416+
/// The path to write to.
417+
path: PathBuf,
418+
/// The current buffer of data.
419+
///
420+
/// This will include data that has been flushed already.
421+
current_data: Vec<u8>,
422+
/// Whether to write to the data or to the meta.
423+
is_meta_writer: bool,
424+
}
425+
426+
impl AsyncWrite for DataWriter {
427+
fn poll_write(
428+
self: Pin<&mut Self>,
429+
_: &mut core::task::Context<'_>,
430+
buf: &[u8],
431+
) -> Poll<std::io::Result<usize>> {
432+
self.get_mut().current_data.extend_from_slice(buf);
433+
Poll::Ready(Ok(buf.len()))
434+
}
435+
436+
fn poll_flush(
437+
self: Pin<&mut Self>,
438+
_: &mut core::task::Context<'_>,
439+
) -> Poll<std::io::Result<()>> {
440+
// Write the data to our fake disk. This means we will repeatedly reinsert the asset.
441+
if self.is_meta_writer {
442+
self.dir.insert_meta(&self.path, self.current_data.clone());
443+
} else {
444+
self.dir.insert_asset(&self.path, self.current_data.clone());
445+
}
446+
Poll::Ready(Ok(()))
447+
}
448+
449+
fn poll_close(
450+
self: Pin<&mut Self>,
451+
cx: &mut core::task::Context<'_>,
452+
) -> Poll<std::io::Result<()>> {
453+
// A flush will just write the data to Dir, which is all we need to do for close.
454+
self.poll_flush(cx)
455+
}
456+
}
457+
458+
impl AssetWriter for MemoryAssetWriter {
459+
async fn write<'a>(&'a self, path: &'a Path) -> Result<Box<super::Writer>, AssetWriterError> {
460+
Ok(Box::new(DataWriter {
461+
dir: self.root.clone(),
462+
path: path.to_owned(),
463+
current_data: vec![],
464+
is_meta_writer: false,
465+
}))
466+
}
467+
468+
async fn write_meta<'a>(
469+
&'a self,
470+
path: &'a Path,
471+
) -> Result<Box<super::Writer>, AssetWriterError> {
472+
Ok(Box::new(DataWriter {
473+
dir: self.root.clone(),
474+
path: path.to_owned(),
475+
current_data: vec![],
476+
is_meta_writer: true,
477+
}))
478+
}
479+
480+
async fn remove<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
481+
if self.root.remove_asset(path).is_none() {
482+
return Err(AssetWriterError::Io(Error::new(
483+
ErrorKind::NotFound,
484+
"no such file",
485+
)));
486+
}
487+
Ok(())
488+
}
489+
490+
async fn remove_meta<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
491+
self.root.remove_metadata(path);
492+
Ok(())
493+
}
494+
495+
async fn rename<'a>(
496+
&'a self,
497+
old_path: &'a Path,
498+
new_path: &'a Path,
499+
) -> Result<(), AssetWriterError> {
500+
let Some(old_asset) = self.root.get_asset(old_path) else {
501+
return Err(AssetWriterError::Io(Error::new(
502+
ErrorKind::NotFound,
503+
"no such file",
504+
)));
505+
};
506+
self.root.insert_asset(new_path, old_asset.value);
507+
// Remove the asset after instead of before since otherwise there'd be a moment where the
508+
// Dir is unlocked and missing both the old and new paths. This just prevents race
509+
// conditions.
510+
self.root.remove_asset(old_path);
511+
Ok(())
512+
}
513+
514+
async fn rename_meta<'a>(
515+
&'a self,
516+
old_path: &'a Path,
517+
new_path: &'a Path,
518+
) -> Result<(), AssetWriterError> {
519+
let Some(old_meta) = self.root.get_metadata(old_path) else {
520+
return Err(AssetWriterError::Io(Error::new(
521+
ErrorKind::NotFound,
522+
"no such file",
523+
)));
524+
};
525+
self.root.insert_meta(new_path, old_meta.value);
526+
// Remove the meta after instead of before since otherwise there'd be a moment where the
527+
// Dir is unlocked and missing both the old and new paths. This just prevents race
528+
// conditions.
529+
self.root.remove_metadata(old_path);
530+
Ok(())
531+
}
532+
533+
async fn create_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
534+
// Just pretend we're on a file system that doesn't consider directory re-creation a
535+
// failure.
536+
self.root.get_or_insert_dir(path);
537+
Ok(())
538+
}
539+
540+
async fn remove_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
541+
if self.root.remove_dir(path).is_none() {
542+
return Err(AssetWriterError::Io(Error::new(
543+
ErrorKind::NotFound,
544+
"no such dir",
545+
)));
546+
}
547+
Ok(())
548+
}
549+
550+
async fn remove_empty_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
551+
let Some(dir) = self.root.get_dir(path) else {
552+
return Err(AssetWriterError::Io(Error::new(
553+
ErrorKind::NotFound,
554+
"no such dir",
555+
)));
556+
};
557+
558+
let dir = dir.0.read().unwrap();
559+
if !dir.assets.is_empty() || !dir.metadata.is_empty() || !dir.dirs.is_empty() {
560+
return Err(AssetWriterError::Io(Error::new(
561+
ErrorKind::DirectoryNotEmpty,
562+
"not empty",
563+
)));
564+
}
565+
566+
self.root.remove_dir(path);
567+
Ok(())
568+
}
569+
570+
async fn remove_assets_in_directory<'a>(
571+
&'a self,
572+
path: &'a Path,
573+
) -> Result<(), AssetWriterError> {
574+
let Some(dir) = self.root.get_dir(path) else {
575+
return Err(AssetWriterError::Io(Error::new(
576+
ErrorKind::NotFound,
577+
"no such dir",
578+
)));
579+
};
580+
581+
let mut dir = dir.0.write().unwrap();
582+
dir.assets.clear();
583+
dir.dirs.clear();
584+
dir.metadata.clear();
585+
Ok(())
586+
}
587+
}
588+
364589
#[cfg(test)]
365590
pub mod test {
366591
use super::Dir;

0 commit comments

Comments
 (0)