chore: use the real file system for testing

This commit is contained in:
anatawa12 2025-04-28 18:30:27 +09:00
commit 1bf81a3b6a
No known key found for this signature in database
GPG key ID: 9CA909848B8E4EA6
10 changed files with 55 additions and 849 deletions

View file

@ -52,6 +52,9 @@ windows = { version = "0.61", features = ["Win32_System_Threading", "Win32_Secur
[target."cfg(target_os = \"macos\")".dependencies]
plist = { version = "1", optional = true }
[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread"] }
[features]
default = ["rustls"]
native-tls = ["reqwest/native-tls-vendored"]

View file

@ -1,5 +1,4 @@
use common::*;
use futures::executor::block_on;
use std::collections::HashSet;
use std::io;
use std::path::Path;
@ -1200,8 +1199,7 @@ fn no_temp_folder_after_add() {
.await
.unwrap();
let env_vfs = VirtualFileSystem::new();
let env = VirtualEnvironment::new(env_vfs);
let env = VirtualInstaller::new();
let resolve = project
.remove_request(&["com.vrchat.avatars"])
@ -1232,6 +1230,7 @@ fn no_temp_folder_after_add() {
}
#[test]
#[ignore = "No suitable way to lock a file"]
fn locked_in_package_folder() {
block_on(async {
let mut project = VirtualProjectBuilder::new()
@ -1251,14 +1250,9 @@ fn locked_in_package_folder() {
.await
.unwrap();
project
.io()
.deny_deletion("Packages/com.vrchat.avatars/content.txt".as_ref())
.await
.unwrap();
//project.io().lock("Packages/com.vrchat.avatars/content.txt".as_ref()).await.unwrap();
let env_vfs = VirtualFileSystem::new();
let env = VirtualEnvironment::new(env_vfs);
let env = VirtualInstaller::new();
let resolve = project
.remove_request(&["com.vrchat.avatars"])

View file

@ -4,13 +4,11 @@
mod package_collection;
mod virtual_environment;
mod virtual_file_system;
mod virtual_project_builder;
pub use package_collection::PackageCollection;
pub use package_collection::PackageCollectionBuilder;
pub use virtual_environment::VirtualEnvironment;
pub use virtual_file_system::VirtualFileSystem;
pub use virtual_environment::VirtualInstaller;
pub use virtual_project_builder::VirtualProjectBuilder;
use vrc_get_vpm::PackageInfo;
@ -95,3 +93,10 @@ pub fn assert_installing_to_dependencies_only(
.expect("not installing to dependencies");
assert_eq!(base_range, &DependencyRange::version(version));
}
pub fn block_on<F: Future>(f: F) -> F::Output {
tokio::runtime::Builder::new_multi_thread()
.build()
.unwrap()
.block_on(f)
}

View file

@ -1,4 +1,4 @@
use crate::common::{PackageCollection, VirtualFileSystem};
use crate::common::PackageCollection;
use indexmap::IndexMap;
use serde_json::json;
use std::future::Future;
@ -12,17 +12,15 @@ use vrc_get_vpm::{
AbortCheck, HttpClient, PackageInfo, PackageInstaller, PackageManifest, UnityProject,
};
pub struct VirtualEnvironment {
vfs: VirtualFileSystem,
}
pub struct VirtualInstaller {}
impl VirtualEnvironment {
pub fn new(vfs: VirtualFileSystem) -> Self {
Self { vfs }
impl VirtualInstaller {
pub fn new() -> Self {
Self {}
}
}
impl PackageInstaller for VirtualEnvironment {
impl PackageInstaller for VirtualInstaller {
fn install_package(
&self,
_: &impl ProjectIo,

View file

@ -1,772 +0,0 @@
use futures::Stream;
use indexmap::IndexMap;
use indexmap::map::Entry as IndexEntry;
use std::ffi::{OsStr, OsString};
use std::future::Future;
use std::io::ErrorKind;
use std::path::{Component, Path, PathBuf};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::{error, io};
use vrc_get_vpm::io::{EnvironmentIo, ExitStatus, FileType, IoTrait, Metadata, ProjectIo};
pub(crate) use file_stream::*;
const NOTA_DIRECTORY: ErrorKind = ErrorKind::Other; // NotADirectory is unstable
const IS_DIRECTORY: ErrorKind = ErrorKind::Other; // IsADirectory is unstable
fn err<V, E>(kind: ErrorKind, error: E) -> io::Result<V>
where
E: Into<Box<dyn error::Error + Send + Sync>>,
{
Err(io::Error::new(kind, error))
}
/// The virtual file system is a TraitIo implementation for testing.
///
/// This struct implements All EnvironmentIo and ProjectIo methods.
pub struct VirtualFileSystem {
root: DirectoryEntry,
}
impl VirtualFileSystem {
pub fn new() -> Self {
Self {
root: DirectoryEntry::new(),
}
}
pub async fn add_file(&self, path: &Path, content: &[u8]) -> io::Result<()> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(IS_DIRECTORY, "is directory");
};
self.root
.create_dir_all(&dir_path)
.await?
.create_file(last, true)
.await?
.set_content(content)
.await;
Ok(())
}
pub async fn deny_deletion(&self, path: &Path) -> io::Result<()> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(IS_DIRECTORY, "is directory");
};
self.root
.get_folder(&dir_path)
.await?
.get(last)
.await?
.as_file()?
.deny_deletion();
Ok(())
}
}
impl VirtualFileSystem {
fn resolve<'a>(&self, path: &'a Path) -> io::Result<Vec<&'a OsStr>> {
let mut result = Vec::new();
for x in path.components() {
match x {
Component::Prefix(_) | Component::RootDir => {
panic!("absolute path")
}
Component::CurDir => continue,
Component::ParentDir => {
if result.pop().is_none() {
panic!("accessing parent folder")
}
}
Component::Normal(component) => result.push(component),
}
}
Ok(result)
}
fn resolve2<'a>(&self, path: &'a Path) -> io::Result<Option<(Vec<&'a OsStr>, &'a OsStr)>> {
let mut resolved = self.resolve(path)?;
if resolved.is_empty() {
Ok(None)
} else {
let last = resolved.remove(resolved.len() - 1);
Ok(Some((resolved, last)))
}
}
}
impl vrc_get_vpm::io::IoTrait for VirtualFileSystem {
async fn create_dir_all(&self, path: &Path) -> io::Result<()> {
self.root.create_dir_all(&self.resolve(path)?).await?;
Ok(())
}
async fn write(&self, path: &Path, content: &[u8]) -> io::Result<()> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(IS_DIRECTORY, "is directory");
};
let file = self
.root
.get_folder(&dir_path)
.await?
.create_file(last, false)
.await?;
file.set_content(content).await;
Ok(())
}
fn write_sync(
&self,
path: &Path,
content: &[u8],
) -> impl Future<Output = io::Result<()>> + Send {
self.write(path, content)
}
async fn remove_file(&self, path: &Path) -> io::Result<()> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(IS_DIRECTORY, "is directory");
};
self.root
.get_folder(&dir_path)
.await?
.remove_file(last)
.await?;
Ok(())
}
async fn remove_dir(&self, path: &Path) -> io::Result<()> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(ErrorKind::PermissionDenied, "removing root");
};
self.root
.get_folder(&dir_path)
.await?
.remove_dir_all(last)
.await?;
Ok(())
}
async fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(ErrorKind::PermissionDenied, "removing root");
};
self.root
.get_folder(&dir_path)
.await?
.remove_dir_all(last)
.await?;
Ok(())
}
async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
let Some((from_dir, from_last)) = self.resolve2(from)? else {
return err(ErrorKind::PermissionDenied, "moving root");
};
let Some((to_dir, to_last)) = self.resolve2(to)? else {
return err(ErrorKind::PermissionDenied, "moving to root");
};
let from_dir = self.root.get_folder(&from_dir).await?;
let to_dir = self.root.get_folder(&to_dir).await?;
let mut from_dir = from_dir.backed.lock().unwrap();
let from_entry = match from_dir.entry(from_last.to_os_string()) {
Entry::Occupied(e) => e,
Entry::Vacant(_) => return err(ErrorKind::NotFound, "file not found"),
};
let original = from_entry.shift_remove();
drop(from_dir);
let mut to_dir = to_dir.backed.lock().unwrap();
let to_entry = match to_dir.entry(to_last.to_os_string()) {
Entry::Occupied(_) => return err(ErrorKind::AlreadyExists, "file exists"),
Entry::Vacant(e) => e,
};
to_entry.insert(original);
drop(to_dir);
Ok(())
}
async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return Ok(Metadata::dir());
};
Ok(self
.root
.get_folder(&dir_path)
.await?
.get(last)
.await?
.metadata())
}
type DirEntry = DirEntry;
type ReadDirStream = ReadDirStream;
async fn read_dir(&self, path: &Path) -> io::Result<Self::ReadDirStream> {
let root = self.root.get_folder(&self.resolve(path)?).await?;
Ok(ReadDirStream::new(root))
}
type FileStream = FileStream;
async fn create_new(&self, path: &Path) -> io::Result<Self::FileStream> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(IS_DIRECTORY, "is directory");
};
Ok(FileStream::new(
self.root
.get_folder(&dir_path)
.await?
.create_file(last, true)
.await?
.content
.clone(),
))
}
async fn create(&self, path: &Path) -> io::Result<Self::FileStream> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(IS_DIRECTORY, "is directory");
};
Ok(FileStream::new(
self.root
.get_folder(&dir_path)
.await?
.create_file(last, false)
.await?
.content
.clone(),
))
}
async fn open(&self, path: &Path) -> io::Result<Self::FileStream> {
let Some((dir_path, last)) = self.resolve2(path)? else {
return err(IS_DIRECTORY, "is directory");
};
Ok(FileStream::new(
self.root
.get_folder(&dir_path)
.await?
.get(last)
.await?
.into_file()?
.content
.clone(),
))
}
}
impl EnvironmentIo for VirtualFileSystem {
fn resolve(&self, path: &Path) -> PathBuf {
self.resolve(path)
.expect("unexpected full path")
.iter()
.collect()
}
#[cfg(feature = "vrc-get-litedb")]
type MutexGuard = ();
#[cfg(feature = "vrc-get-litedb")]
async fn new_mutex(&self, _: &OsStr) -> io::Result<Self::MutexGuard> {
err(ErrorKind::Unsupported, "shared mutex")
}
#[cfg(feature = "experimental-project-management")]
type ProjectIo = VirtualFileSystem;
#[cfg(feature = "experimental-project-management")]
fn new_project_io(&self, _: &Path) -> Self::ProjectIo {
panic!("not implemented") // TODO: implement
}
}
impl ProjectIo for VirtualFileSystem {}
#[derive(Clone)]
enum FileSystemEntry {
File(FileEntry),
Directory(DirectoryEntry),
}
impl FileSystemEntry {
fn metadata(&self) -> Metadata {
match self {
FileSystemEntry::File(_) => Metadata::file(),
FileSystemEntry::Directory(_) => Metadata::dir(),
}
}
fn into_file(self) -> io::Result<FileEntry> {
match self {
FileSystemEntry::File(e) => Ok(e),
FileSystemEntry::Directory(_) => err(IS_DIRECTORY, "is a directory"),
}
}
fn into_directory(self) -> io::Result<DirectoryEntry> {
match self {
FileSystemEntry::File(_) => err(NOTA_DIRECTORY, "is a file"),
FileSystemEntry::Directory(e) => Ok(e),
}
}
fn as_file(&self) -> io::Result<&FileEntry> {
match self {
FileSystemEntry::File(e) => Ok(e),
FileSystemEntry::Directory(_) => err(IS_DIRECTORY, "is a directory"),
}
}
fn as_directory(&self) -> io::Result<&DirectoryEntry> {
match self {
FileSystemEntry::File(_) => err(NOTA_DIRECTORY, "is a file"),
FileSystemEntry::Directory(e) => Ok(e),
}
}
}
#[derive(Clone)]
struct DirectoryEntry {
backed: Arc<Mutex<DirectoryContent>>,
}
struct DirectoryContent {
content: IndexMap<OsString, Option<FileSystemEntry>>,
}
impl DirectoryContent {
fn new() -> Self {
Self {
content: IndexMap::new(),
}
}
fn is_empty(&self) -> bool {
self.content.is_empty() || self.content.values().all(Option::is_none)
}
fn get(&self, name: &OsStr) -> Option<&FileSystemEntry> {
self.content.get(name)?.as_ref()
}
fn entry(&mut self, name: OsString) -> Entry {
Entry::new(self.content.entry(name))
}
fn last(&self) -> Option<(&OsString, &FileSystemEntry)> {
self.content
.iter()
.rev()
.find_map(|(k, v)| v.as_ref().map(|v| (k, v)))
}
fn remove_last(&mut self) {
if let Some(slot) = self.content.values_mut().rev().find(|v| v.is_some()) {
*slot = None;
}
}
fn get_index_internal(&self, index: usize) -> Option<(&OsString, Option<&FileSystemEntry>)> {
self.content.get_index(index).map(|(k, v)| (k, v.as_ref()))
}
}
enum Entry<'a> {
Occupied(OccupiedEntry<'a>),
Vacant(VacantEntry<'a>),
}
impl<'a> Entry<'a> {
fn new(entry: IndexEntry<'a, OsString, Option<FileSystemEntry>>) -> Self {
match entry {
IndexEntry::Occupied(e) => match e.get() {
Some(_) => Entry::Occupied(OccupiedEntry(e)),
None => Entry::Vacant(VacantEntry::Occupied(e)),
},
IndexEntry::Vacant(e) => Entry::Vacant(VacantEntry::Vacant(e)),
}
}
}
struct OccupiedEntry<'a>(indexmap::map::OccupiedEntry<'a, OsString, Option<FileSystemEntry>>);
impl<'a> OccupiedEntry<'a> {
fn into_mut(self) -> &'a mut FileSystemEntry {
self.0.into_mut().as_mut().unwrap()
}
fn shift_remove(self) -> FileSystemEntry {
self.0.into_mut().take().unwrap()
}
fn get_mut(&mut self) -> &mut FileSystemEntry {
self.0.get_mut().as_mut().unwrap()
}
}
enum VacantEntry<'a> {
Occupied(indexmap::map::OccupiedEntry<'a, OsString, Option<FileSystemEntry>>),
Vacant(indexmap::map::VacantEntry<'a, OsString, Option<FileSystemEntry>>),
}
impl<'a> VacantEntry<'a> {
fn insert(self, entry: FileSystemEntry) -> &'a mut FileSystemEntry {
match self {
VacantEntry::Occupied(e) => e.into_mut().insert(entry),
VacantEntry::Vacant(e) => e.insert(Some(entry)).as_mut().unwrap(),
}
}
}
impl DirectoryEntry {
fn new() -> Self {
Self {
backed: Arc::new(Mutex::new(DirectoryContent::new())),
}
}
async fn get(&self, name: &OsStr) -> io::Result<FileSystemEntry> {
self.backed
.lock()
.unwrap()
.get(name)
.cloned()
.ok_or_else(|| io::Error::new(ErrorKind::NotFound, "file not found"))
}
async fn get_folder(&self, path: &[&OsStr]) -> io::Result<DirectoryEntry> {
let mut current = self.clone();
for component in path {
current = current.get(component).await?.into_directory()?;
}
Ok(current)
}
async fn create_dir_all(&self, path: &[&OsStr]) -> io::Result<DirectoryEntry> {
let mut current = self.clone();
for component in path {
let mut locked = current.backed.lock().unwrap();
let next = match locked.entry(component.to_os_string()) {
Entry::Occupied(e) => e.into_mut().as_directory()?.clone(),
Entry::Vacant(e) => e
.insert(FileSystemEntry::Directory(DirectoryEntry::new()))
.as_directory()
.unwrap()
.clone(),
};
drop(locked);
current = next;
}
Ok(current)
}
async fn create_file(&self, name: &OsStr, new: bool) -> io::Result<FileEntry> {
let mut backed = self.backed.lock().unwrap();
match backed.entry(name.to_os_string()) {
Entry::Occupied(e) => match e.into_mut() {
FileSystemEntry::File(_) if new => err(ErrorKind::AlreadyExists, "file exists"),
FileSystemEntry::File(entry) => Ok(entry.clone()),
FileSystemEntry::Directory(_) => err(IS_DIRECTORY, "directory exists"),
},
Entry::Vacant(e) => {
let FileSystemEntry::File(e) = e.insert(FileSystemEntry::File(FileEntry::new()))
else {
unreachable!()
};
Ok(e.clone())
}
}
}
async fn remove_file(&self, name: &OsStr) -> io::Result<FileEntry> {
let mut backed = self.backed.lock().unwrap();
match backed.entry(name.to_os_string()) {
Entry::Occupied(mut e) => {
let file = e.get_mut().as_file()?;
if !file.can_remove() {
return err(ErrorKind::PermissionDenied, "file is locked");
}
Ok(e.shift_remove().into_file().unwrap())
}
Entry::Vacant(_) => err(ErrorKind::NotFound, "file not found"),
}
}
async fn remove_dir(&self, name: &OsStr) -> io::Result<DirectoryEntry> {
let mut backed = self.backed.lock().unwrap();
match backed.entry(name.to_os_string()) {
Entry::Occupied(mut e) => {
let as_dir = e.get_mut().as_directory()?;
if !as_dir.backed.lock().unwrap().is_empty() {
return err(ErrorKind::Other, "directory not empty"); // DirectoryNotEmpty is unstable
}
Ok(e.shift_remove().into_directory().unwrap())
}
Entry::Vacant(_) => err(ErrorKind::NotFound, "file not found"),
}
}
async fn remove_dir_all(&self, name: &OsStr) -> io::Result<DirectoryEntry> {
let mut backed = self.backed.lock().unwrap();
match backed.entry(name.to_os_string()) {
Entry::Occupied(mut e) => {
e.get_mut().as_directory()?.clear()?;
Ok(e.shift_remove().into_directory().unwrap())
}
Entry::Vacant(_) => err(ErrorKind::NotFound, "file not found"),
}
}
fn clear(&self) -> io::Result<()> {
let mut backed = self.backed.lock().unwrap();
while let Some((_, child)) = backed.last() {
match child {
FileSystemEntry::File(f) => {
if !f.can_remove() {
return err(ErrorKind::PermissionDenied, "file is locked");
}
}
FileSystemEntry::Directory(d) => d.clear()?,
}
backed.remove_last();
}
Ok(())
}
}
#[derive(Clone)]
struct FileEntry {
content: Arc<Mutex<FileContent>>,
}
struct FileContent {
content: Vec<u8>,
locked: bool,
}
impl FileContent {
fn new() -> Self {
Self {
content: Vec::new(),
locked: false,
}
}
}
impl FileEntry {
fn new() -> Self {
Self {
content: Arc::new(Mutex::new(FileContent::new())),
}
}
pub(crate) async fn set_content(&self, content: &[u8]) {
self.content.lock().unwrap().content = content.to_vec();
}
pub(crate) fn deny_deletion(&self) {
self.content.lock().unwrap().locked = true;
}
pub(crate) fn can_remove(&self) -> bool {
!self.content.lock().unwrap().locked
}
}
pub struct ReadDirStream {
index: usize,
dir: DirectoryEntry,
}
impl ReadDirStream {
fn new(dir: DirectoryEntry) -> Self {
Self { index: 0, dir }
}
}
impl Stream for ReadDirStream {
type Item = io::Result<DirEntry>;
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context) -> Poll<Option<Self::Item>> {
let locked = self.dir.backed.lock().unwrap();
let mut index = self.index;
loop {
let Some((name, entry)) = locked.get_index_internal(index) else {
drop(locked);
self.index = index;
return Poll::Ready(None);
};
index += 1;
let Some(entry) = entry else {
continue;
};
let entry = DirEntry::new(name, entry.metadata());
drop(locked);
self.index = index;
return Poll::Ready(Some(Ok(entry)));
}
}
}
pub struct DirEntry {
name: OsString,
metadata: Metadata,
}
impl DirEntry {
fn new(name: &OsStr, metadata: Metadata) -> Self {
Self {
name: name.to_os_string(),
metadata,
}
}
}
impl vrc_get_vpm::io::DirEntry for DirEntry {
fn file_name(&self) -> OsString {
self.name.clone()
}
async fn file_type(&self) -> io::Result<FileType> {
Ok(self.metadata.file_type())
}
async fn metadata(&self) -> io::Result<Metadata> {
Ok(self.metadata.clone())
}
}
mod file_stream {
use crate::common::virtual_file_system::FileContent;
use futures::{AsyncRead, AsyncSeek, AsyncWrite};
use std::future::Future;
use std::io;
use std::io::{ErrorKind, SeekFrom};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
pub struct FileStream {
content: Arc<Mutex<FileContent>>,
position: usize,
}
impl FileStream {
pub(super) fn new(content: Arc<Mutex<FileContent>>) -> Self {
Self {
content,
position: 0,
}
}
}
impl AsyncSeek for FileStream {
fn poll_seek(
mut self: Pin<&mut Self>,
_: &mut Context<'_>,
pos: SeekFrom,
) -> Poll<io::Result<u64>> {
match pos {
SeekFrom::Start(position) => {
self.position = position
.try_into()
.map_err(|_| io::Error::new(ErrorKind::InvalidInput, "invalid position"))?;
Poll::Ready(Ok(self.position as u64))
}
SeekFrom::Current(offset) => {
let offset = offset
.try_into()
.map_err(|_| io::Error::new(ErrorKind::InvalidInput, "invalid position"))?;
self.position = self.position.checked_add_signed(offset).ok_or_else(|| {
io::Error::new(ErrorKind::InvalidInput, "invalid position")
})?;
Poll::Ready(Ok(self.position as u64))
}
SeekFrom::End(offset) => {
let offset = offset
.try_into()
.map_err(|_| io::Error::new(ErrorKind::InvalidInput, "invalid position"))?;
let lock = self.content.clone();
let guard = lock.lock().unwrap();
self.position =
guard
.content
.len()
.checked_add_signed(offset)
.ok_or_else(|| {
io::Error::new(ErrorKind::InvalidInput, "invalid position")
})?;
Poll::Ready(Ok(self.position as u64))
}
}
}
}
impl AsyncRead for FileStream {
fn poll_read(
mut self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let lock = self.content.clone();
let guard = lock.lock().unwrap();
let len = guard.content.len();
let remaining = len - self.position;
let to_copy = buf.len().min(remaining);
buf[..to_copy].copy_from_slice(&guard.content[self.position..][..to_copy]);
self.position += to_copy;
Poll::Ready(Ok(to_copy))
}
}
impl AsyncWrite for FileStream {
fn poll_write(
self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let lock = self.content.clone();
let mut guard = lock.lock().unwrap();
let new_len = self.position + buf.len();
if new_len > guard.content.len() {
guard.content.resize(new_len, 0);
}
guard.content[self.position..][..buf.len()].copy_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
impl vrc_get_vpm::io::FileStream for FileStream {}
}

View file

@ -1,7 +1,7 @@
use crate::common::VirtualFileSystem;
use indexmap::IndexMap;
use serde_json::json;
use vrc_get_vpm::io::IoTrait;
use std::path::{Path, PathBuf};
use vrc_get_vpm::io::{DefaultProjectIo, IoTrait};
use vrc_get_vpm::unity_project::pending_project_changes::Remove;
use vrc_get_vpm::version::{Version, VersionRange};
use vrc_get_vpm::{PackageManifest, UnityProject};
@ -88,7 +88,21 @@ impl VirtualProjectBuilder {
self
}
pub async fn build(&self) -> std::io::Result<UnityProject<VirtualFileSystem>> {
#[track_caller]
pub fn build(&self) -> impl Future<Output = std::io::Result<UnityProject<DefaultProjectIo>>> {
let project_path = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!(
"test_projects/{}_L{}",
env!("CARGO_CRATE_NAME"),
std::panic::Location::caller().line()
));
self.build_impl(project_path)
}
async fn build_impl(
&self,
project_path: PathBuf,
) -> std::io::Result<UnityProject<DefaultProjectIo>> {
let vpm_manifest = {
let mut dependencies = serde_json::Map::new();
for (dependency, version) in &self.dependencies {
@ -116,15 +130,22 @@ impl VirtualProjectBuilder {
})
};
let fs = VirtualFileSystem::new();
fs.add_file(
"Packages/vpm-manifest.json".as_ref(),
match tokio::fs::remove_dir_all(&project_path).await {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Ok(()) => {}
Err(e) => return Err(e),
}
tokio::fs::create_dir_all(&project_path.join("Packages")).await?;
tokio::fs::write(
project_path.join("Packages/vpm-manifest.json"),
vpm_manifest.to_string().as_bytes(),
)
.await?;
fs.add_file(
"ProjectSettings/ProjectVersion.txt".as_ref(),
tokio::fs::create_dir_all(&project_path.join("ProjectSettings")).await?;
tokio::fs::write(
project_path.join("ProjectSettings/ProjectVersion.txt"),
format!(
"m_EditorVersion: {version}\n\
m_EditorVersionWithRevision: {version} ({revision})\n\
@ -137,13 +158,14 @@ impl VirtualProjectBuilder {
.await?;
for (name, contents) in &self.files {
fs.add_file(name.as_ref(), contents.as_bytes()).await?;
tokio::fs::create_dir_all(&project_path.join(name).parent().unwrap()).await?;
tokio::fs::write(project_path.join(name), contents).await?;
}
for name in &self.directories {
fs.create_dir_all(name.as_ref()).await?;
tokio::fs::create_dir_all(project_path.join(name)).await?;
}
UnityProject::load(fs).await
UnityProject::load(DefaultProjectIo::new(project_path.into())).await
}
}

View file

@ -1,5 +1,4 @@
use crate::common::VirtualProjectBuilder;
use futures::executor::block_on;
use crate::common::*;
use vrc_get_vpm::version::{ReleaseType, UnityVersion, Version};
mod common;

View file

@ -1,5 +1,4 @@
use crate::common::*;
use futures::executor::block_on;
use vrc_get_vpm::unity_project::pending_project_changes::RemoveReason;
use vrc_get_vpm::version::Version;

View file

@ -1,5 +1,4 @@
use crate::common::*;
use futures::executor::block_on;
use vrc_get_vpm::PackageManifest;
use vrc_get_vpm::version::Version;

View file

@ -1,41 +0,0 @@
//! This file contains tests for the virtual file system.
use futures::AsyncReadExt;
use vrc_get_vpm::io::IoTrait;
mod common;
#[test]
fn test() {
futures::executor::block_on(async {
let vfs = common::VirtualFileSystem::new();
let content = b"";
vfs.add_file("test".as_ref(), content).await.unwrap();
let mut read = Vec::new();
vfs.open("test".as_ref())
.await
.unwrap()
.read_to_end(&mut read)
.await
.unwrap();
assert_eq!(read, content);
});
}
#[test]
fn remove_dir_contains_forbidden() {
futures::executor::block_on(async {
let vfs = common::VirtualFileSystem::new();
let content = b"";
vfs.add_file("test-dir/forbidden-file".as_ref(), content)
.await
.unwrap();
vfs.deny_deletion("test-dir/forbidden-file".as_ref())
.await
.unwrap();
vfs.remove_dir_all("test-dir".as_ref()).await.unwrap_err();
});
}