Skip to content

Commit 7ba5baf

Browse files
committed
Create a MemoryAssetWriter to allow testing savers and processors.
1 parent cd44b30 commit 7ba5baf

File tree

1 file changed

+229
-7
lines changed

1 file changed

+229
-7
lines changed

crates/bevy_asset/src/io/memory.rs

Lines changed: 229 additions & 7 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 {
@@ -296,8 +341,8 @@ impl AsyncSeekForward for DataReader {
296341
self.bytes_read = new_pos as _;
297342
Poll::Ready(Ok(new_pos as _))
298343
} else {
299-
Poll::Ready(Err(std::io::Error::new(
300-
std::io::ErrorKind::InvalidInput,
344+
Poll::Ready(Err(Error::new(
345+
ErrorKind::InvalidInput,
301346
"seek position is out of range",
302347
)))
303348
}
@@ -361,6 +406,183 @@ impl AssetReader for MemoryAssetReader {
361406
}
362407
}
363408

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

0 commit comments

Comments
 (0)