mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
feat: add in-memory executor filesystem
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
use super::*;
|
||||
use crate::config::ConfigBuilder;
|
||||
use codex_exec_server::InMemoryFileSystem;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::Feature;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::TempDirExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -85,6 +87,24 @@ async fn make_config_with_project_root_markers(
|
||||
config
|
||||
}
|
||||
|
||||
async fn make_virtual_config(
|
||||
cwd: AbsolutePathBuf,
|
||||
limit: usize,
|
||||
instructions: Option<&str>,
|
||||
) -> Config {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("defaults for test should always succeed");
|
||||
|
||||
config.cwd = cwd;
|
||||
config.project_doc_max_bytes = limit;
|
||||
config.user_instructions = instructions.map(ToOwned::to_owned);
|
||||
config
|
||||
}
|
||||
|
||||
/// AGENTS.md missing – should yield `None`.
|
||||
#[tokio::test]
|
||||
async fn no_doc_file_returns_none() {
|
||||
@@ -295,6 +315,30 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
|
||||
assert_eq!(res, "parent doc\n\nchild doc");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn injected_filesystem_supports_virtual_absolute_agents_paths() {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let repo_root =
|
||||
AbsolutePathBuf::try_from(test_path_buf("/virtual/repo")).expect("absolute repo root");
|
||||
let nested = AbsolutePathBuf::try_from(test_path_buf("/virtual/repo/workspace/crate_a"))
|
||||
.expect("absolute nested path");
|
||||
assert!(!repo_root.as_path().exists());
|
||||
|
||||
fs.seed_file(&repo_root.join(".git"), b"gitdir: fake\n".to_vec())
|
||||
.expect("seed repo marker");
|
||||
fs.seed_file(&repo_root.join("AGENTS.md"), b"root doc".to_vec())
|
||||
.expect("seed root doc");
|
||||
fs.seed_file(&nested.join("AGENTS.md"), b"crate doc".to_vec())
|
||||
.expect("seed nested doc");
|
||||
|
||||
let cfg = make_virtual_config(nested, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
let res = AgentsMdManager::new(&cfg)
|
||||
.user_instructions_with_fs(&fs)
|
||||
.await
|
||||
.expect("virtual doc expected");
|
||||
assert_eq!(res, "root doc\n\ncrate doc");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn instruction_sources_include_global_before_agents_md_docs() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -36,6 +36,7 @@ pub use codex_file_system::ExecutorFileSystem;
|
||||
pub use codex_file_system::FileMetadata;
|
||||
pub use codex_file_system::FileSystemResult;
|
||||
pub use codex_file_system::FileSystemSandboxContext;
|
||||
pub use codex_file_system::InMemoryFileSystem;
|
||||
pub use codex_file_system::ReadDirectoryEntry;
|
||||
pub use codex_file_system::RemoveOptions;
|
||||
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
|
||||
@@ -3,4 +3,9 @@ load("//:defs.bzl", "codex_rust_crate")
|
||||
codex_rust_crate(
|
||||
name = "file-system",
|
||||
crate_name = "codex_file_system",
|
||||
deps_extra = [
|
||||
"@crates//:pretty_assertions",
|
||||
"@crates//:tempfile",
|
||||
"@crates//:tokio",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -13,6 +13,11 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
use crate::CopyOptions;
|
||||
use crate::CreateDirectoryOptions;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::FileMetadata;
|
||||
use crate::FileSystemResult;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::ReadDirectoryEntry;
|
||||
use crate::RemoveOptions;
|
||||
use async_trait::async_trait;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum EntryKind {
|
||||
Directory,
|
||||
File(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Entry {
|
||||
kind: EntryKind,
|
||||
created_at_ms: i64,
|
||||
modified_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
entries: BTreeMap<AbsolutePathBuf, Entry>,
|
||||
next_timestamp_ms: i64,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn next_timestamp_ms(&mut self) -> i64 {
|
||||
let timestamp = self.next_timestamp_ms;
|
||||
self.next_timestamp_ms += 1;
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure in-memory implementation of [`ExecutorFileSystem`] for tests and
|
||||
/// injected-fs call sites that must avoid host filesystem access.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InMemoryFileSystem {
|
||||
state: RwLock<State>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: BTreeMap::new(),
|
||||
next_timestamp_ms: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InMemoryFileSystem {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn seed_directory(&self, path: &AbsolutePathBuf) -> io::Result<()> {
|
||||
let mut state = self.state.write().expect("in-memory fs write lock");
|
||||
create_directory_all(&mut state, path)
|
||||
}
|
||||
|
||||
pub fn seed_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: impl Into<Vec<u8>>,
|
||||
) -> io::Result<()> {
|
||||
let mut state = self.state.write().expect("in-memory fs write lock");
|
||||
ensure_parent_directories(&mut state, path)?;
|
||||
if is_directory(&state, path) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
format!("path already exists: {}", path.display()),
|
||||
));
|
||||
}
|
||||
let timestamp = state.next_timestamp_ms();
|
||||
state.entries.insert(
|
||||
path.clone(),
|
||||
Entry {
|
||||
kind: EntryKind::File(contents.into()),
|
||||
created_at_ms: timestamp,
|
||||
modified_at_ms: timestamp,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exists(&self, path: &AbsolutePathBuf) -> bool {
|
||||
let state = self.state.read().expect("in-memory fs read lock");
|
||||
entry_exists(&state, path)
|
||||
}
|
||||
|
||||
pub fn file_contents(&self, path: &AbsolutePathBuf) -> Option<Vec<u8>> {
|
||||
let state = self.state.read().expect("in-memory fs read lock");
|
||||
match state.entries.get(path) {
|
||||
Some(Entry {
|
||||
kind: EntryKind::File(contents),
|
||||
..
|
||||
}) => Some(contents.clone()),
|
||||
Some(Entry {
|
||||
kind: EntryKind::Directory,
|
||||
..
|
||||
})
|
||||
| None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for InMemoryFileSystem {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let state = self.state.read().expect("in-memory fs read lock");
|
||||
match state.entries.get(path) {
|
||||
Some(Entry {
|
||||
kind: EntryKind::File(contents),
|
||||
..
|
||||
}) => Ok(contents.clone()),
|
||||
Some(Entry {
|
||||
kind: EntryKind::Directory,
|
||||
..
|
||||
}) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"cannot read a directory as a file",
|
||||
)),
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("path not found: {}", path.display()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let mut state = self.state.write().expect("in-memory fs write lock");
|
||||
ensure_file_parent_exists(&state, path)?;
|
||||
if is_directory(&state, path) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"cannot overwrite a directory with a file",
|
||||
));
|
||||
}
|
||||
let timestamp = state.next_timestamp_ms();
|
||||
let created_at_ms = state
|
||||
.entries
|
||||
.get(path)
|
||||
.map(|entry| entry.created_at_ms)
|
||||
.unwrap_or(timestamp);
|
||||
state.entries.insert(
|
||||
path.clone(),
|
||||
Entry {
|
||||
kind: EntryKind::File(contents),
|
||||
created_at_ms,
|
||||
modified_at_ms: timestamp,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let mut state = self.state.write().expect("in-memory fs write lock");
|
||||
if is_root(path) {
|
||||
return if options.recursive {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
format!("directory already exists: {}", path.display()),
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(entry) = state.entries.get(path) {
|
||||
return if matches!(entry.kind, EntryKind::Directory) && options.recursive {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
format!("path already exists: {}", path.display()),
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if options.recursive {
|
||||
create_directory_all(&mut state, path)
|
||||
} else {
|
||||
ensure_directory_parent_exists(&state, path)?;
|
||||
insert_directory(&mut state, path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let state = self.state.read().expect("in-memory fs read lock");
|
||||
if is_root(path) {
|
||||
return Ok(directory_metadata(0, 0));
|
||||
}
|
||||
|
||||
let entry = state.entries.get(path).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("path not found: {}", path.display()),
|
||||
)
|
||||
})?;
|
||||
Ok(match &entry.kind {
|
||||
EntryKind::Directory => directory_metadata(entry.created_at_ms, entry.modified_at_ms),
|
||||
EntryKind::File(_) => FileMetadata {
|
||||
is_directory: false,
|
||||
is_file: true,
|
||||
is_symlink: false,
|
||||
created_at_ms: entry.created_at_ms,
|
||||
modified_at_ms: entry.modified_at_ms,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let state = self.state.read().expect("in-memory fs read lock");
|
||||
if !is_root(path) && !is_directory(&state, path) {
|
||||
return if state.entries.contains_key(path) {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"cannot read directory entries from a file",
|
||||
))
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("path not found: {}", path.display()),
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for (entry_path, entry) in &state.entries {
|
||||
if entry_path.parent().as_ref() != Some(path) {
|
||||
continue;
|
||||
}
|
||||
entries.push(ReadDirectoryEntry {
|
||||
file_name: file_name(entry_path)?,
|
||||
is_directory: matches!(entry.kind, EntryKind::Directory),
|
||||
is_file: matches!(entry.kind, EntryKind::File(_)),
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
if is_root(path) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"cannot remove the virtual filesystem root",
|
||||
));
|
||||
}
|
||||
|
||||
let mut state = self.state.write().expect("in-memory fs write lock");
|
||||
let Some(entry) = state.entries.get(path).cloned() else {
|
||||
return if options.force {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("path not found: {}", path.display()),
|
||||
))
|
||||
};
|
||||
};
|
||||
|
||||
match entry.kind {
|
||||
EntryKind::File(_) => {
|
||||
state.entries.remove(path);
|
||||
Ok(())
|
||||
}
|
||||
EntryKind::Directory => {
|
||||
let has_children = state
|
||||
.entries
|
||||
.keys()
|
||||
.any(|candidate| candidate.parent().as_ref() == Some(path));
|
||||
if has_children && !options.recursive {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::DirectoryNotEmpty,
|
||||
format!("directory is not empty: {}", path.display()),
|
||||
));
|
||||
}
|
||||
state
|
||||
.entries
|
||||
.retain(|candidate, _| !candidate.starts_with(path));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
if destination_path == source_path || destination_path.starts_with(source_path) {
|
||||
let source_is_directory = {
|
||||
let state = self.state.read().expect("in-memory fs read lock");
|
||||
is_root(source_path) || is_directory(&state, source_path)
|
||||
};
|
||||
if source_is_directory {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"fs/copy cannot copy a directory to itself or one of its descendants",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = {
|
||||
let state = self.state.read().expect("in-memory fs read lock");
|
||||
snapshot_entry(&state, source_path)?
|
||||
};
|
||||
|
||||
let mut state = self.state.write().expect("in-memory fs write lock");
|
||||
match snapshot {
|
||||
SnapshotEntry::File { contents } => {
|
||||
ensure_file_parent_exists(&state, destination_path)?;
|
||||
if is_directory(&state, destination_path) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"cannot copy a file over a directory",
|
||||
));
|
||||
}
|
||||
let timestamp = state.next_timestamp_ms();
|
||||
state.entries.insert(
|
||||
destination_path.clone(),
|
||||
Entry {
|
||||
kind: EntryKind::File(contents),
|
||||
created_at_ms: timestamp,
|
||||
modified_at_ms: timestamp,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
SnapshotEntry::Directory { entries } => {
|
||||
if !options.recursive {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"fs/copy requires recursive: true when sourcePath is a directory",
|
||||
));
|
||||
}
|
||||
create_directory_all(&mut state, destination_path)?;
|
||||
for (source_entry, snapshot_entry) in entries {
|
||||
let relative =
|
||||
source_entry
|
||||
.strip_prefix(source_path.as_path())
|
||||
.map_err(|err| {
|
||||
io::Error::other(format!("failed to strip prefix: {err}"))
|
||||
})?;
|
||||
let destination_entry = destination_path.join(relative);
|
||||
match snapshot_entry {
|
||||
SnapshotDirectoryEntry::Directory => {
|
||||
create_directory_all(&mut state, &destination_entry)?;
|
||||
}
|
||||
SnapshotDirectoryEntry::File(contents) => {
|
||||
ensure_parent_directories(&mut state, &destination_entry)?;
|
||||
let timestamp = state.next_timestamp_ms();
|
||||
state.entries.insert(
|
||||
destination_entry,
|
||||
Entry {
|
||||
kind: EntryKind::File(contents),
|
||||
created_at_ms: timestamp,
|
||||
modified_at_ms: timestamp,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SnapshotEntry {
|
||||
File {
|
||||
contents: Vec<u8>,
|
||||
},
|
||||
Directory {
|
||||
entries: Vec<(AbsolutePathBuf, SnapshotDirectoryEntry)>,
|
||||
},
|
||||
}
|
||||
|
||||
enum SnapshotDirectoryEntry {
|
||||
Directory,
|
||||
File(Vec<u8>),
|
||||
}
|
||||
|
||||
fn snapshot_entry(state: &State, source_path: &AbsolutePathBuf) -> io::Result<SnapshotEntry> {
|
||||
if is_root(source_path) || is_directory(state, source_path) {
|
||||
let mut entries = Vec::new();
|
||||
for (path, entry) in &state.entries {
|
||||
if !path.starts_with(source_path) || path == source_path {
|
||||
continue;
|
||||
}
|
||||
entries.push((
|
||||
path.clone(),
|
||||
match &entry.kind {
|
||||
EntryKind::Directory => SnapshotDirectoryEntry::Directory,
|
||||
EntryKind::File(contents) => SnapshotDirectoryEntry::File(contents.clone()),
|
||||
},
|
||||
));
|
||||
}
|
||||
return Ok(SnapshotEntry::Directory { entries });
|
||||
}
|
||||
|
||||
match state.entries.get(source_path) {
|
||||
Some(Entry {
|
||||
kind: EntryKind::File(contents),
|
||||
..
|
||||
}) => Ok(SnapshotEntry::File {
|
||||
contents: contents.clone(),
|
||||
}),
|
||||
Some(Entry {
|
||||
kind: EntryKind::Directory,
|
||||
..
|
||||
}) => unreachable!("directory case handled above"),
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("path not found: {}", source_path.display()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_directory_all(state: &mut State, path: &AbsolutePathBuf) -> io::Result<()> {
|
||||
if is_root(path) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
create_directory_all(state, &parent)?;
|
||||
}
|
||||
if let Some(entry) = state.entries.get(path) {
|
||||
return if matches!(entry.kind, EntryKind::Directory) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
format!("path already exists: {}", path.display()),
|
||||
))
|
||||
};
|
||||
}
|
||||
insert_directory(state, path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_directory(state: &mut State, path: &AbsolutePathBuf) {
|
||||
let timestamp = state.next_timestamp_ms();
|
||||
state.entries.insert(
|
||||
path.clone(),
|
||||
Entry {
|
||||
kind: EntryKind::Directory,
|
||||
created_at_ms: timestamp,
|
||||
modified_at_ms: timestamp,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn ensure_parent_directories(state: &mut State, path: &AbsolutePathBuf) -> io::Result<()> {
|
||||
let Some(parent) = path.parent() else {
|
||||
return Ok(());
|
||||
};
|
||||
create_directory_all(state, &parent)
|
||||
}
|
||||
|
||||
fn ensure_file_parent_exists(state: &State, path: &AbsolutePathBuf) -> io::Result<()> {
|
||||
ensure_directory_parent_exists(state, path)
|
||||
}
|
||||
|
||||
fn ensure_directory_parent_exists(state: &State, path: &AbsolutePathBuf) -> io::Result<()> {
|
||||
let Some(parent) = path.parent() else {
|
||||
return Ok(());
|
||||
};
|
||||
if is_root(&parent) || is_directory(state, &parent) {
|
||||
return Ok(());
|
||||
}
|
||||
if state.entries.contains_key(&parent) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("parent is not a directory: {}", parent.display()),
|
||||
));
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("parent directory not found: {}", parent.display()),
|
||||
))
|
||||
}
|
||||
|
||||
fn directory_metadata(created_at_ms: i64, modified_at_ms: i64) -> FileMetadata {
|
||||
FileMetadata {
|
||||
is_directory: true,
|
||||
is_file: false,
|
||||
is_symlink: false,
|
||||
created_at_ms,
|
||||
modified_at_ms,
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_exists(state: &State, path: &AbsolutePathBuf) -> bool {
|
||||
is_root(path) || state.entries.contains_key(path)
|
||||
}
|
||||
|
||||
fn is_directory(state: &State, path: &AbsolutePathBuf) -> bool {
|
||||
is_root(path)
|
||||
|| matches!(
|
||||
state.entries.get(path),
|
||||
Some(Entry {
|
||||
kind: EntryKind::Directory,
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn is_root(path: &AbsolutePathBuf) -> bool {
|
||||
path.parent().is_none()
|
||||
}
|
||||
|
||||
fn file_name(path: &AbsolutePathBuf) -> io::Result<String> {
|
||||
path.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"path does not have a file name",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
|
||||
if sandbox.is_some() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"in-memory filesystem operations do not accept sandbox context",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,6 +12,10 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
mod in_memory;
|
||||
|
||||
pub use in_memory::InMemoryFileSystem;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct CreateDirectoryOptions {
|
||||
pub recursive: bool,
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
use codex_file_system::CopyOptions;
|
||||
use codex_file_system::CreateDirectoryOptions;
|
||||
use codex_file_system::ExecutorFileSystem;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_file_system::InMemoryFileSystem;
|
||||
use codex_file_system::RemoveOptions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn path(unix_path: &str) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::try_from(test_path_buf(unix_path)).expect("test path should be absolute")
|
||||
}
|
||||
|
||||
fn path_buf(path: PathBuf) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::try_from(path).expect("path should be absolute")
|
||||
}
|
||||
|
||||
fn sandbox_context() -> FileSystemSandboxContext {
|
||||
FileSystemSandboxContext::from_permission_profile(PermissionProfile::default())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_write_and_inspection_work_for_virtual_file_paths() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let root = path("/virtual/project");
|
||||
let file = path("/virtual/project/notes.txt");
|
||||
|
||||
fs.seed_directory(&root)?;
|
||||
assert!(fs.exists(&root));
|
||||
|
||||
fs.write_file(&file, b"hello".to_vec(), /*sandbox*/ None)
|
||||
.await?;
|
||||
|
||||
assert_eq!(fs.read_file(&file, /*sandbox*/ None).await?, b"hello");
|
||||
assert_eq!(fs.file_contents(&file), Some(b"hello".to_vec()));
|
||||
assert!(fs.exists(&file));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reads_do_not_fall_back_to_host_files() -> io::Result<()> {
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
let host_file = tempdir.path().join("host-only.txt");
|
||||
std::fs::write(&host_file, "host contents")?;
|
||||
let host_file = path_buf(host_file);
|
||||
|
||||
let err = InMemoryFileSystem::new()
|
||||
.read_file(&host_file, /*sandbox*/ None)
|
||||
.await
|
||||
.expect_err("host-only file should be invisible");
|
||||
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_paths_exist_without_explicit_seeding() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let root = path("/");
|
||||
|
||||
assert!(fs.exists(&root));
|
||||
assert_eq!(
|
||||
fs.get_metadata(&root, /*sandbox*/ None).await?,
|
||||
codex_file_system::FileMetadata {
|
||||
is_directory: true,
|
||||
is_file: false,
|
||||
is_symlink: false,
|
||||
created_at_ms: 0,
|
||||
modified_at_ms: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
fs.read_directory(&root, /*sandbox*/ None).await?,
|
||||
Vec::new()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn directory_metadata_and_read_directory_are_deterministic() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let dir = path("/virtual/project");
|
||||
|
||||
fs.seed_file(&path("/virtual/project/zeta.txt"), b"z".to_vec())?;
|
||||
fs.seed_directory(&path("/virtual/project/alpha"))?;
|
||||
fs.seed_file(&path("/virtual/project/middle.txt"), b"m".to_vec())?;
|
||||
|
||||
let metadata = fs.get_metadata(&dir, /*sandbox*/ None).await?;
|
||||
assert_eq!(metadata.is_directory, true);
|
||||
assert_eq!(metadata.is_file, false);
|
||||
assert_eq!(metadata.is_symlink, false);
|
||||
|
||||
let entries = fs.read_directory(&dir, /*sandbox*/ None).await?;
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
codex_file_system::ReadDirectoryEntry {
|
||||
file_name: "alpha".to_string(),
|
||||
is_directory: true,
|
||||
is_file: false,
|
||||
},
|
||||
codex_file_system::ReadDirectoryEntry {
|
||||
file_name: "middle.txt".to_string(),
|
||||
is_directory: false,
|
||||
is_file: true,
|
||||
},
|
||||
codex_file_system::ReadDirectoryEntry {
|
||||
file_name: "zeta.txt".to_string(),
|
||||
is_directory: false,
|
||||
is_file: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_directory_only_returns_immediate_children() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let dir = path("/virtual/project");
|
||||
|
||||
fs.seed_file(&path("/virtual/project/nested/deep.txt"), b"deep".to_vec())?;
|
||||
fs.seed_file(&path("/virtual/project/top.txt"), b"top".to_vec())?;
|
||||
|
||||
assert_eq!(
|
||||
fs.read_directory(&dir, /*sandbox*/ None).await?,
|
||||
vec![
|
||||
codex_file_system::ReadDirectoryEntry {
|
||||
file_name: "nested".to_string(),
|
||||
is_directory: true,
|
||||
is_file: false,
|
||||
},
|
||||
codex_file_system::ReadDirectoryEntry {
|
||||
file_name: "top.txt".to_string(),
|
||||
is_directory: false,
|
||||
is_file: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_directory_honors_recursive_behavior() {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let deep_dir = path("/virtual/project/a/b");
|
||||
|
||||
let err = fs
|
||||
.create_directory(
|
||||
&deep_dir,
|
||||
CreateDirectoryOptions { recursive: false },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("non-recursive create should require existing parent");
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
|
||||
fs.create_directory(
|
||||
&deep_dir,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("recursive create should build ancestors");
|
||||
|
||||
assert!(fs.exists(&path("/virtual/project/a")));
|
||||
assert!(fs.exists(&deep_dir));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_directory_handles_existing_entries() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let dir = path("/virtual/project");
|
||||
|
||||
fs.seed_directory(&dir)?;
|
||||
fs.create_directory(
|
||||
&dir,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let err = fs
|
||||
.create_directory(
|
||||
&dir,
|
||||
CreateDirectoryOptions { recursive: false },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("non-recursive create should reject existing directory");
|
||||
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
|
||||
|
||||
fs.seed_file(&path("/virtual/project/file.txt"), b"file".to_vec())?;
|
||||
let err = fs
|
||||
.create_directory(
|
||||
&path("/virtual/project/file.txt"),
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("directory create should reject existing file");
|
||||
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_file_requires_existing_parent_directory() {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let err = fs
|
||||
.write_file(
|
||||
&path("/virtual/project/missing/notes.txt"),
|
||||
b"hello".to_vec(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("write should not create arbitrary parent directories");
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_writes_preserve_created_at_and_update_modified_at() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let file = path("/virtual/project/notes.txt");
|
||||
|
||||
fs.seed_directory(&path("/virtual/project"))?;
|
||||
fs.write_file(&file, b"first".to_vec(), /*sandbox*/ None)
|
||||
.await?;
|
||||
let first = fs.get_metadata(&file, /*sandbox*/ None).await?;
|
||||
|
||||
fs.write_file(&file, b"second".to_vec(), /*sandbox*/ None)
|
||||
.await?;
|
||||
let second = fs.get_metadata(&file, /*sandbox*/ None).await?;
|
||||
|
||||
assert_eq!(second.created_at_ms, first.created_at_ms);
|
||||
assert!(second.modified_at_ms > first.modified_at_ms);
|
||||
assert_eq!(fs.file_contents(&file), Some(b"second".to_vec()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_and_seed_file_reject_directory_targets() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let dir = path("/virtual/project");
|
||||
|
||||
fs.seed_directory(&dir)?;
|
||||
|
||||
let err = fs
|
||||
.write_file(&dir, b"contents".to_vec(), /*sandbox*/ None)
|
||||
.await
|
||||
.expect_err("write should reject directory target");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.seed_file(&dir, b"contents".to_vec())
|
||||
.expect_err("seed should reject directory target");
|
||||
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_file_and_read_directory_reject_wrong_entry_kinds() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let dir = path("/virtual/project");
|
||||
let file = path("/virtual/project/notes.txt");
|
||||
|
||||
fs.seed_file(&file, b"hello".to_vec())?;
|
||||
|
||||
let err = fs
|
||||
.read_file(&dir, /*sandbox*/ None)
|
||||
.await
|
||||
.expect_err("reading a directory as a file should fail");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.read_directory(&file, /*sandbox*/ None)
|
||||
.await
|
||||
.expect_err("reading a file as a directory should fail");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_file_text_rejects_invalid_utf8() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let file = path("/virtual/project/bytes.bin");
|
||||
|
||||
fs.seed_file(&file, vec![0xff])?;
|
||||
let err = fs
|
||||
.read_file_text(&file, /*sandbox*/ None)
|
||||
.await
|
||||
.expect_err("invalid utf8 should fail");
|
||||
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remove_honors_recursive_and_force_behavior() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let dir = path("/virtual/project");
|
||||
|
||||
fs.seed_file(&path("/virtual/project/child.txt"), b"c".to_vec())?;
|
||||
|
||||
let err = fs
|
||||
.remove(
|
||||
&dir,
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
force: false,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("non-recursive remove should reject non-empty directories");
|
||||
assert_eq!(err.kind(), io::ErrorKind::DirectoryNotEmpty);
|
||||
|
||||
fs.remove(
|
||||
&dir,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
force: false,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert!(!fs.exists(&dir));
|
||||
|
||||
fs.remove(
|
||||
&path("/virtual/project/missing.txt"),
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
force: true,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remove_supports_files_and_rejects_root() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let file = path("/virtual/project/file.txt");
|
||||
|
||||
fs.seed_file(&file, b"payload".to_vec())?;
|
||||
fs.remove(
|
||||
&file,
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
force: false,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert!(!fs.exists(&file));
|
||||
|
||||
let err = fs
|
||||
.remove(
|
||||
&path("/"),
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
force: false,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("root removal should fail");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_supports_files_and_recursive_directories() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let source_dir = path("/virtual/source");
|
||||
let source_file = path("/virtual/source/file.txt");
|
||||
|
||||
fs.seed_file(&source_file, b"payload".to_vec())?;
|
||||
fs.seed_file(
|
||||
&path("/virtual/source/nested/inner.txt"),
|
||||
b"nested".to_vec(),
|
||||
)?;
|
||||
|
||||
fs.copy(
|
||||
&source_file,
|
||||
&path("/virtual/copied.txt"),
|
||||
CopyOptions { recursive: false },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
fs.read_file(&path("/virtual/copied.txt"), /*sandbox*/ None)
|
||||
.await?,
|
||||
b"payload"
|
||||
);
|
||||
|
||||
let err = fs
|
||||
.copy(
|
||||
&source_dir,
|
||||
&path("/virtual/dir-copy"),
|
||||
CopyOptions { recursive: false },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("directory copy requires recursive");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
fs.copy(
|
||||
&source_dir,
|
||||
&path("/virtual/dir-copy"),
|
||||
CopyOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
fs.read_file(
|
||||
&path("/virtual/dir-copy/nested/inner.txt"),
|
||||
/*sandbox*/ None
|
||||
)
|
||||
.await?,
|
||||
b"nested"
|
||||
);
|
||||
|
||||
let err = fs
|
||||
.copy(
|
||||
&source_dir,
|
||||
&path("/virtual/source/descendant"),
|
||||
CopyOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("copying a directory into a descendant should fail");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_handles_empty_directories_and_missing_sources() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let source_dir = path("/virtual/source");
|
||||
let empty_dir = path("/virtual/source/empty");
|
||||
|
||||
fs.seed_directory(&empty_dir)?;
|
||||
fs.copy(
|
||||
&source_dir,
|
||||
&path("/virtual/destination"),
|
||||
CopyOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert!(fs.exists(&path("/virtual/destination/empty")));
|
||||
|
||||
let err = fs
|
||||
.copy(
|
||||
&path("/virtual/missing"),
|
||||
&path("/virtual/destination/missing"),
|
||||
CopyOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("copy should reject missing source");
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_rejects_file_over_directory() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let source_file = path("/virtual/source/file.txt");
|
||||
let destination_dir = path("/virtual/destination");
|
||||
|
||||
fs.seed_file(&source_file, b"payload".to_vec())?;
|
||||
fs.seed_directory(&destination_dir)?;
|
||||
|
||||
let err = fs
|
||||
.copy(
|
||||
&source_file,
|
||||
&destination_dir,
|
||||
CopyOptions { recursive: false },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("copy should reject file over directory");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inspection_helpers_report_missing_paths_and_directories() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let dir = path("/virtual/project");
|
||||
let missing = path("/virtual/missing");
|
||||
|
||||
fs.seed_directory(&dir)?;
|
||||
|
||||
assert!(fs.exists(&dir));
|
||||
assert!(!fs.exists(&missing));
|
||||
assert_eq!(fs.file_contents(&dir), None);
|
||||
assert_eq!(fs.file_contents(&missing), None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_context_is_rejected_for_all_operations() -> io::Result<()> {
|
||||
let fs = InMemoryFileSystem::new();
|
||||
let sandbox = sandbox_context();
|
||||
let dir = path("/virtual/project");
|
||||
let file = path("/virtual/project/file.txt");
|
||||
|
||||
fs.seed_file(&file, b"contents".to_vec())?;
|
||||
|
||||
let err = fs
|
||||
.read_file(&file, Some(&sandbox))
|
||||
.await
|
||||
.expect_err("read_file should reject sandbox");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.write_file(&file, b"contents".to_vec(), Some(&sandbox))
|
||||
.await
|
||||
.expect_err("write_file should reject sandbox");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.create_directory(
|
||||
&dir,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
Some(&sandbox),
|
||||
)
|
||||
.await
|
||||
.expect_err("create_directory should reject sandbox");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.get_metadata(&dir, Some(&sandbox))
|
||||
.await
|
||||
.expect_err("get_metadata should reject sandbox");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.read_directory(&dir, Some(&sandbox))
|
||||
.await
|
||||
.expect_err("read_directory should reject sandbox");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.remove(
|
||||
&file,
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
force: false,
|
||||
},
|
||||
Some(&sandbox),
|
||||
)
|
||||
.await
|
||||
.expect_err("remove should reject sandbox");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
let err = fs
|
||||
.copy(
|
||||
&file,
|
||||
&path("/virtual/project/copy.txt"),
|
||||
CopyOptions { recursive: false },
|
||||
Some(&sandbox),
|
||||
)
|
||||
.await
|
||||
.expect_err("copy should reject sandbox");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user