feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo
- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt - Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk - Convert git dependencies to path dependencies - Add vendor crates to workspace members - Remove test/example crates from vendor (not needed for SDK) - Fix feature propagation issues detected by zepter - Fix workspace inheritance for internal dependencies - All 606 crates now in workspace - All 6919 internal dependency links verified correct - No git dependencies remaining
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "zombienet-support"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Support crates with common traits/structs and helpers"
|
||||
keywords = ["zombienet"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
nix = { workspace = true, features = ["signal"] }
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -0,0 +1,24 @@
|
||||
pub const VALID_REGEX: &str = "regex should be valid ";
|
||||
pub const BORROWABLE: &str = "must be borrowable as mutable ";
|
||||
pub const RELAY_NOT_NONE: &str = "typestate should ensure the relaychain isn't None at this point ";
|
||||
pub const SHOULD_COMPILE: &str = "should compile with success ";
|
||||
pub const INFAILABLE: &str = "infaillible ";
|
||||
pub const NO_ERR_DEF_BUILDER: &str = "should have no errors for default builder ";
|
||||
pub const RW_FAILED: &str = "should be able to read/write - failed ";
|
||||
pub const DEFAULT_TYPESTATE: &str = "'default' overriding should be ensured by typestate ";
|
||||
pub const VALIDATION_CHECK: &str = "validation failed ";
|
||||
|
||||
pub const PREFIX_CANT_BE_NONE: &str = "name prefix can't be None if a value exists ";
|
||||
|
||||
pub const GRAPH_CONTAINS_NAME: &str =
|
||||
"graph contains node name; we initialize it with all node names";
|
||||
pub const GRAPH_CONTAINS_DEP: &str = "graph contains dep_name; we filter out deps not contained in by_name and populate the graph with all nodes";
|
||||
pub const INDEGREE_CONTAINS_NAME: &str =
|
||||
"indegree contains node name; we initialize it with all node names";
|
||||
pub const QUEUE_NOT_EMPTY: &str = "queue is not empty; we're looping over its length";
|
||||
|
||||
pub const THIS_IS_A_BUG: &str =
|
||||
"- this is a bug please report it: https://github.com/paritytech/zombienet-sdk/issues";
|
||||
|
||||
/// environment variable which can be used to override node spawn timeout
|
||||
pub const ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS: &str = "ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS";
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub mod in_memory;
|
||||
pub mod local;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error(transparent)]
|
||||
pub struct FileSystemError(#[from] anyhow::Error);
|
||||
|
||||
impl From<std::io::Error> for FileSystemError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self(error.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub type FileSystemResult<T> = Result<T, FileSystemError>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait FileSystem {
|
||||
async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send;
|
||||
|
||||
async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send;
|
||||
|
||||
async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
|
||||
where
|
||||
P: AsRef<Path> + Send;
|
||||
|
||||
async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
|
||||
where
|
||||
P: AsRef<Path> + Send;
|
||||
|
||||
async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
C: AsRef<[u8]> + Send;
|
||||
|
||||
async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
C: AsRef<[u8]> + Send;
|
||||
|
||||
async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
|
||||
where
|
||||
P1: AsRef<Path> + Send,
|
||||
P2: AsRef<Path> + Send;
|
||||
|
||||
async fn set_mode<P>(&self, path: P, perm: u32) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send;
|
||||
|
||||
async fn exists<P>(&self, path: P) -> bool
|
||||
where
|
||||
P: AsRef<Path> + Send;
|
||||
}
|
||||
@@ -0,0 +1,879 @@
|
||||
use std::{collections::HashMap, ffi::OsString, path::Path, sync::Arc};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::{FileSystem, FileSystemResult};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum InMemoryFile {
|
||||
File { mode: u32, contents: Vec<u8> },
|
||||
Directory { mode: u32 },
|
||||
}
|
||||
|
||||
impl InMemoryFile {
|
||||
pub fn file<C>(contents: C) -> Self
|
||||
where
|
||||
C: AsRef<str>,
|
||||
{
|
||||
Self::file_raw(contents.as_ref())
|
||||
}
|
||||
|
||||
pub fn file_raw<C>(contents: C) -> Self
|
||||
where
|
||||
C: AsRef<[u8]>,
|
||||
{
|
||||
Self::File {
|
||||
mode: 0o664,
|
||||
contents: contents.as_ref().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::file_raw(vec![])
|
||||
}
|
||||
|
||||
pub fn dir() -> Self {
|
||||
Self::Directory { mode: 0o775 }
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> u32 {
|
||||
match *self {
|
||||
Self::File { mode, .. } => mode,
|
||||
Self::Directory { mode, .. } => mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contents_raw(&self) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
Self::File { contents, .. } => Some(contents.to_vec()),
|
||||
Self::Directory { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contents(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::File { contents, .. } => Some(String::from_utf8_lossy(contents).to_string()),
|
||||
Self::Directory { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct InMemoryFileSystem {
|
||||
pub files: Arc<RwLock<HashMap<OsString, InMemoryFile>>>,
|
||||
}
|
||||
|
||||
impl InMemoryFileSystem {
|
||||
pub fn new(files: HashMap<OsString, InMemoryFile>) -> Self {
|
||||
Self {
|
||||
files: Arc::new(RwLock::new(files)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FileSystem for InMemoryFileSystem {
|
||||
async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let os_path = path.as_os_str();
|
||||
match self.files.read().await.get(os_path) {
|
||||
Some(InMemoryFile::File { .. }) => {
|
||||
Err(anyhow!("file {:?} already exists", os_path.to_owned(),))?
|
||||
},
|
||||
Some(InMemoryFile::Directory { .. }) => {
|
||||
Err(anyhow!("directory {:?} already exists", os_path.to_owned(),))?
|
||||
},
|
||||
None => {},
|
||||
};
|
||||
|
||||
for path in path.ancestors().skip(1) {
|
||||
match self.files.read().await.get(path.as_os_str()) {
|
||||
Some(InMemoryFile::Directory { .. }) => continue,
|
||||
Some(InMemoryFile::File { .. }) => Err(anyhow!(
|
||||
"ancestor {:?} is not a directory",
|
||||
path.as_os_str(),
|
||||
))?,
|
||||
None => Err(anyhow!("ancestor {:?} doesn't exists", path.as_os_str(),))?,
|
||||
};
|
||||
}
|
||||
|
||||
self.files
|
||||
.write()
|
||||
.await
|
||||
.insert(os_path.to_owned(), InMemoryFile::dir());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let mut files = self.files.write().await;
|
||||
let ancestors = path
|
||||
.ancestors()
|
||||
.collect::<Vec<&Path>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.skip(1);
|
||||
|
||||
for path in ancestors {
|
||||
match files.get(path.as_os_str()) {
|
||||
Some(InMemoryFile::Directory { .. }) => continue,
|
||||
Some(InMemoryFile::File { .. }) => Err(anyhow!(
|
||||
"ancestor {:?} is not a directory",
|
||||
path.as_os_str().to_owned(),
|
||||
))?,
|
||||
None => files.insert(path.as_os_str().to_owned(), InMemoryFile::dir()),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
let os_path = path.as_ref().as_os_str();
|
||||
|
||||
match self.files.read().await.get(os_path) {
|
||||
Some(InMemoryFile::File { contents, .. }) => Ok(contents.clone()),
|
||||
Some(InMemoryFile::Directory { .. }) => {
|
||||
Err(anyhow!("file {os_path:?} is a directory").into())
|
||||
},
|
||||
None => Err(anyhow!("file {os_path:?} not found").into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
let os_path = path.as_ref().as_os_str().to_owned();
|
||||
let content = self.read(path).await?;
|
||||
|
||||
String::from_utf8(content)
|
||||
.map_err(|_| anyhow!("invalid utf-8 encoding for file {os_path:?}").into())
|
||||
}
|
||||
|
||||
async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
C: AsRef<[u8]> + Send,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let os_path = path.as_os_str();
|
||||
let mut files = self.files.write().await;
|
||||
|
||||
for path in path.ancestors().skip(1) {
|
||||
match files.get(path.as_os_str()) {
|
||||
Some(InMemoryFile::Directory { .. }) => continue,
|
||||
Some(InMemoryFile::File { .. }) => Err(anyhow!(
|
||||
"ancestor {:?} is not a directory",
|
||||
path.as_os_str()
|
||||
))?,
|
||||
None => Err(anyhow!("ancestor {:?} doesn't exists", path.as_os_str()))?,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(InMemoryFile::Directory { .. }) = files.get(os_path) {
|
||||
return Err(anyhow!("file {os_path:?} is a directory").into());
|
||||
}
|
||||
|
||||
files.insert(os_path.to_owned(), InMemoryFile::file_raw(contents));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
C: AsRef<[u8]> + Send,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let mut existing_contents = match self.read(path).await {
|
||||
Ok(existing_contents) => existing_contents,
|
||||
Err(err) if err.to_string() == format!("file {:?} not found", path.as_os_str()) => {
|
||||
vec![]
|
||||
},
|
||||
Err(err) => Err(err)?,
|
||||
};
|
||||
existing_contents.append(&mut contents.as_ref().to_vec());
|
||||
|
||||
self.write(path, existing_contents).await
|
||||
}
|
||||
|
||||
async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
|
||||
where
|
||||
P1: AsRef<Path> + Send,
|
||||
P2: AsRef<Path> + Send,
|
||||
{
|
||||
let from_ref = from.as_ref();
|
||||
let to_ref = to.as_ref();
|
||||
let content = self.read(from_ref).await?;
|
||||
|
||||
self.write(to_ref, content).await
|
||||
}
|
||||
|
||||
async fn set_mode<P>(&self, path: P, mode: u32) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
let os_path = path.as_ref().as_os_str();
|
||||
if let Some(file) = self.files.write().await.get_mut(os_path) {
|
||||
match file {
|
||||
InMemoryFile::File { mode: old_mode, .. } => {
|
||||
*old_mode = mode;
|
||||
},
|
||||
InMemoryFile::Directory { mode: old_mode, .. } => {
|
||||
*old_mode = mode;
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("file {os_path:?} not found").into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn exists<P>(&self, path: P) -> bool
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
self.files
|
||||
.read()
|
||||
.await
|
||||
.contains_key(path.as_ref().as_os_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_create_a_directory_at_root() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
fs.create_dir("/dir").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/dir").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode } if *mode == 0o775
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_return_an_error_if_directory_already_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/dir").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs.create_dir("/dir").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert_eq!(err.to_string(), "directory \"/dir\" already exists");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_return_an_error_if_file_already_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/dir").unwrap(), InMemoryFile::empty()),
|
||||
]));
|
||||
|
||||
let err = fs.create_dir("/dir").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert_eq!(err.to_string(), "file \"/dir\" already exists");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_create_a_directory_if_all_ancestors_exist() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/path/to/my").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
),
|
||||
]));
|
||||
|
||||
fs.create_dir("/path/to/my/dir").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 5);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/path/to/my/dir").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode} if *mode == 0o775
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_return_an_error_if_some_directory_ancestor_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs.create_dir("/path/to/my/dir").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 3);
|
||||
assert_eq!(err.to_string(), "ancestor \"/path/to/my\" doesn't exists");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_return_an_error_if_some_ancestor_is_not_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/path/to/my").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
),
|
||||
]));
|
||||
|
||||
let err = fs.create_dir("/path/to/my/dir").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 4);
|
||||
assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_all_should_create_a_directory_and_all_its_ancestors_if_they_dont_exist() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
fs.create_dir_all("/path/to/my/dir").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 5);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/path").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode } if *mode == 0o775
|
||||
));
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/path/to").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode } if *mode == 0o775
|
||||
));
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/path/to/my").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode } if *mode == 0o775
|
||||
));
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/path/to/my/dir").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode } if *mode == 0o775
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_all_should_create_a_directory_and_some_of_its_ancestors_if_they_dont_exist()
|
||||
{
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
fs.create_dir_all("/path/to/my/dir").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 5);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/path/to/my").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode } if *mode == 0o775
|
||||
));
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/path/to/my/dir").unwrap())
|
||||
.unwrap(),
|
||||
InMemoryFile::Directory { mode } if *mode == 0o775
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_all_should_return_an_error_if_some_ancestor_is_not_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs.create_dir_all("/path/to/my/dir").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 3);
|
||||
assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_should_return_the_file_content() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("content"),
|
||||
)]));
|
||||
|
||||
let content = fs.read("/myfile").await.unwrap();
|
||||
|
||||
assert_eq!(content, "content".as_bytes().to_vec());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_should_return_an_error_if_file_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::new());
|
||||
|
||||
let err = fs.read("/myfile").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" not found");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_should_return_an_error_if_file_is_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
let err = fs.read("/myfile").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_to_string_should_return_the_file_content_as_a_string() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("content"),
|
||||
)]));
|
||||
|
||||
let content = fs.read_to_string("/myfile").await.unwrap();
|
||||
|
||||
assert_eq!(content, "content");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_to_string_should_return_an_error_if_file_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::new());
|
||||
|
||||
let err = fs.read_to_string("/myfile").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" not found");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_to_string_should_return_an_error_if_file_is_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
let err = fs.read_to_string("/myfile").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_to_string_should_return_an_error_if_file_isnt_utf8_encoded() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file_raw(vec![0xC3, 0x28]),
|
||||
)]));
|
||||
|
||||
let err = fs.read_to_string("/myfile").await.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"invalid utf-8 encoding for file \"/myfile\""
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_create_file_with_content_if_file_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
fs.write("/myfile", "my file content").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/myfile").unwrap()),
|
||||
Some(InMemoryFile::File {mode, contents, .. }) if *mode == 0o664 && contents == "my file content".as_bytes()
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_overwrite_file_content_if_file_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
]));
|
||||
|
||||
fs.write("/myfile", "my new file content").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/myfile").unwrap()),
|
||||
Some(InMemoryFile::File { mode, contents, .. }) if *mode == 0o664 && contents == "my new file content".as_bytes()
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_return_an_error_if_file_is_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/myfile").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs.write("/myfile", "my file content").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_return_an_error_if_file_is_new_and_some_ancestor_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs
|
||||
.write("/path/to/myfile", "my file content")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert_eq!(err.to_string(), "ancestor \"/path\" doesn't exists");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_return_an_error_if_file_is_new_and_some_ancestor_is_not_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs
|
||||
.write("/path/to/myfile", "my file content")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 3);
|
||||
assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_update_file_content_if_file_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
]));
|
||||
|
||||
fs.append("/myfile", " has been updated with new things")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/myfile").unwrap()),
|
||||
Some(InMemoryFile::File { mode, contents, .. }) if *mode == 0o664 && contents == "my file content has been updated with new things".as_bytes()
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_create_file_with_content_if_file_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
fs.append("/myfile", "my file content").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert!(matches!(
|
||||
fs.files
|
||||
.read()
|
||||
.await
|
||||
.get(&OsString::from_str("/myfile").unwrap()),
|
||||
Some(InMemoryFile::File { mode,contents, .. }) if *mode == 0o664 && contents == "my file content".as_bytes()
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_return_an_error_if_file_is_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
let err = fs.append("/myfile", "my file content").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_return_an_error_if_file_is_new_and_some_ancestor_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs
|
||||
.append("/path/to/myfile", "my file content")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert_eq!(err.to_string(), "ancestor \"/path\" doesn't exists");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_return_an_error_if_file_is_new_and_some_ancestor_is_not_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
|
||||
(OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs
|
||||
.append("/path/to/myfile", "my file content")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 3);
|
||||
assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_creates_new_destination_file_if_it_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
]));
|
||||
|
||||
fs.copy("/myfile", "/myfilecopy").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 3);
|
||||
assert!(
|
||||
matches!(fs.files.read().await.get(&OsString::from_str("/myfilecopy").unwrap()).unwrap(), InMemoryFile::File { mode, contents, .. } if *mode == 0o664 && contents == "my file content".as_bytes())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_updates_the_file_content_of_the_destination_file_if_it_already_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my new file content"),
|
||||
),
|
||||
(
|
||||
OsString::from_str("/myfilecopy").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
]));
|
||||
|
||||
fs.copy("/myfile", "/myfilecopy").await.unwrap();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 3);
|
||||
assert!(
|
||||
matches!(fs.files.read().await.get(&OsString::from_str("/myfilecopy").unwrap()).unwrap(), InMemoryFile::File { mode, contents, .. } if *mode == 0o664 && contents == "my new file content".as_bytes())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_returns_an_error_if_source_file_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
|
||||
let err = fs.copy("/myfile", "/mfilecopy").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" not found");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_returns_an_error_if_source_file_is_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/myfile").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
|
||||
let err = fs.copy("/myfile", "/mfilecopy").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_returns_an_error_if_destination_file_is_a_directory() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
(
|
||||
OsString::from_str("/myfilecopy").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
),
|
||||
]));
|
||||
|
||||
let err = fs.copy("/myfile", "/myfilecopy").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfilecopy\" is a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_returns_an_error_if_destination_file_is_new_and_some_ancestor_doesnt_exists(
|
||||
) {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
]));
|
||||
|
||||
let err = fs.copy("/myfile", "/somedir/myfilecopy").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 2);
|
||||
assert_eq!(err.to_string(), "ancestor \"/somedir\" doesn't exists");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_returns_an_error_if_destination_file_is_new_and_some_ancestor_is_not_a_directory(
|
||||
) {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
(
|
||||
OsString::from_str("/mypath").unwrap(),
|
||||
InMemoryFile::empty(),
|
||||
),
|
||||
]));
|
||||
|
||||
let err = fs.copy("/myfile", "/mypath/myfilecopy").await.unwrap_err();
|
||||
|
||||
assert_eq!(fs.files.read().await.len(), 3);
|
||||
assert_eq!(err.to_string(), "ancestor \"/mypath\" is not a directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_mode_should_update_the_file_mode_at_path() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(
|
||||
OsString::from_str("/myfile").unwrap(),
|
||||
InMemoryFile::file("my file content"),
|
||||
),
|
||||
]));
|
||||
assert!(
|
||||
matches!(fs.files.read().await.get(&OsString::from_str("/myfile").unwrap()).unwrap(), InMemoryFile::File { mode, .. } if *mode == 0o664)
|
||||
);
|
||||
|
||||
fs.set_mode("/myfile", 0o400).await.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(fs.files.read().await.get(&OsString::from_str("/myfile").unwrap()).unwrap(), InMemoryFile::File { mode, .. } if *mode == 0o400)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_mode_should_update_the_directory_mode_at_path() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([
|
||||
(OsString::from_str("/").unwrap(), InMemoryFile::dir()),
|
||||
(OsString::from_str("/mydir").unwrap(), InMemoryFile::dir()),
|
||||
]));
|
||||
assert!(
|
||||
matches!(fs.files.read().await.get(&OsString::from_str("/mydir").unwrap()).unwrap(), InMemoryFile::Directory { mode } if *mode == 0o775)
|
||||
);
|
||||
|
||||
fs.set_mode("/mydir", 0o700).await.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(fs.files.read().await.get(&OsString::from_str("/mydir").unwrap()).unwrap(), InMemoryFile::Directory { mode } if *mode == 0o700)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_mode_should_returns_an_error_if_file_doesnt_exists() {
|
||||
let fs = InMemoryFileSystem::new(HashMap::from([(
|
||||
OsString::from_str("/").unwrap(),
|
||||
InMemoryFile::dir(),
|
||||
)]));
|
||||
// intentionally forget to create file
|
||||
|
||||
let err = fs.set_mode("/myfile", 0o400).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "file \"/myfile\" not found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::Path};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use super::{FileSystem, FileSystemError, FileSystemResult};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct LocalFileSystem;
|
||||
|
||||
#[async_trait]
|
||||
impl FileSystem for LocalFileSystem {
|
||||
async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
tokio::fs::create_dir(path).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
tokio::fs::create_dir_all(path).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
tokio::fs::read(path).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
tokio::fs::read_to_string(path).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
C: AsRef<[u8]> + Send,
|
||||
{
|
||||
tokio::fs::write(path, contents).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
C: AsRef<[u8]> + Send,
|
||||
{
|
||||
let contents = contents.as_ref();
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.await
|
||||
.map_err(Into::<FileSystemError>::into)?;
|
||||
|
||||
file.write_all(contents)
|
||||
.await
|
||||
.map_err(Into::<FileSystemError>::into)?;
|
||||
|
||||
file.flush().await.and(Ok(())).map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
|
||||
where
|
||||
P1: AsRef<Path> + Send,
|
||||
P2: AsRef<Path> + Send,
|
||||
{
|
||||
tokio::fs::copy(from, to)
|
||||
.await
|
||||
.and(Ok(()))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn set_mode<P>(&self, path: P, mode: u32) -> FileSystemResult<()>
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
tokio::fs::set_permissions(path, Permissions::from_mode(mode))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn exists<P>(&self, path: P) -> bool
|
||||
where
|
||||
P: AsRef<Path> + Send,
|
||||
{
|
||||
path.as_ref().exists()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
const FILE_BITS: u32 = 0o100000;
|
||||
const DIR_BITS: u32 = 0o40000;
|
||||
|
||||
fn setup() -> String {
|
||||
let test_dir = format!("/tmp/unit_test_{}", Uuid::new_v4());
|
||||
std::fs::create_dir(&test_dir).unwrap();
|
||||
test_dir
|
||||
}
|
||||
|
||||
fn teardown(test_dir: String) {
|
||||
std::fs::remove_dir_all(test_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_create_a_new_directory_at_path() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let new_dir = format!("{test_dir}/mynewdir");
|
||||
fs.create_dir(&new_dir).await.unwrap();
|
||||
|
||||
let new_dir_path = Path::new(&new_dir);
|
||||
assert!(new_dir_path.exists() && new_dir_path.is_dir());
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let new_dir = format!("{test_dir}/mynewdir");
|
||||
// intentionally create new dir before calling function to force error
|
||||
std::fs::create_dir(&new_dir).unwrap();
|
||||
let err = fs.create_dir(&new_dir).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "File exists (os error 17)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_all_should_create_a_new_directory_and_all_of_it_ancestors_at_path() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let new_dir = format!("{test_dir}/the/path/to/mynewdir");
|
||||
fs.create_dir_all(&new_dir).await.unwrap();
|
||||
|
||||
let new_dir_path = Path::new(&new_dir);
|
||||
assert!(new_dir_path.exists() && new_dir_path.is_dir());
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_dir_all_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let new_dir = format!("{test_dir}/the/path/to/mynewdir");
|
||||
// intentionally create new file as ancestor before calling function to force error
|
||||
std::fs::write(format!("{test_dir}/the"), b"test").unwrap();
|
||||
let err = fs.create_dir_all(&new_dir).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "Not a directory (os error 20)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_should_return_the_contents_of_the_file_at_path() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
std::fs::write(&file_path, b"Test").unwrap();
|
||||
let contents = fs.read(file_path).await.unwrap();
|
||||
|
||||
assert_eq!(contents, b"Test");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
// intentionally forget to create file to force error
|
||||
let err = fs.read(file_path).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "No such file or directory (os error 2)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_to_string_should_return_the_contents_of_the_file_at_path_as_string() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
std::fs::write(&file_path, b"Test").unwrap();
|
||||
let contents = fs.read_to_string(file_path).await.unwrap();
|
||||
|
||||
assert_eq!(contents, "Test");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_to_string_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
// intentionally forget to create file to force error
|
||||
let err = fs.read_to_string(file_path).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "No such file or directory (os error 2)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_create_a_new_file_at_path_with_contents() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
fs.write(&file_path, "Test").await.unwrap();
|
||||
|
||||
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_overwrite_an_existing_file_with_contents() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
std::fs::write(&file_path, "Test").unwrap();
|
||||
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
|
||||
fs.write(&file_path, "Test updated").await.unwrap();
|
||||
|
||||
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
// intentionally create directory instead of file to force error
|
||||
std::fs::create_dir(&file_path).unwrap();
|
||||
let err = fs.write(&file_path, "Test").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "Is a directory (os error 21)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_create_a_new_file_at_path_with_contents() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
fs.append(&file_path, "Test").await.unwrap();
|
||||
|
||||
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_updates_an_existing_file_by_appending_contents() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
std::fs::write(&file_path, "Test").unwrap();
|
||||
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
|
||||
fs.append(&file_path, " updated").await.unwrap();
|
||||
|
||||
assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let file_path = format!("{test_dir}/myfile");
|
||||
// intentionally create directory instead of file to force error
|
||||
std::fs::create_dir(&file_path).unwrap();
|
||||
let err = fs.append(&file_path, "Test").await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "Is a directory (os error 21)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_create_a_duplicate_of_source() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let from_path = format!("{test_dir}/myfile");
|
||||
std::fs::write(&from_path, "Test").unwrap();
|
||||
let to_path = format!("{test_dir}/mycopy");
|
||||
fs.copy(&from_path, &to_path).await.unwrap();
|
||||
|
||||
assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Test");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_ovewrite_destination_if_alread_exists() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let from_path = format!("{test_dir}/myfile");
|
||||
std::fs::write(&from_path, "Test").unwrap();
|
||||
let to_path = format!("{test_dir}/mycopy");
|
||||
std::fs::write(&from_path, "Some content").unwrap();
|
||||
fs.copy(&from_path, &to_path).await.unwrap();
|
||||
|
||||
assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Some content");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
|
||||
let from_path = format!("{test_dir}/nonexistentfile");
|
||||
let to_path = format!("{test_dir}/mycopy");
|
||||
let err = fs.copy(&from_path, &to_path).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "No such file or directory (os error 2)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_mode_should_update_the_file_mode_at_path() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
let path = format!("{test_dir}/myfile");
|
||||
std::fs::write(&path, "Test").unwrap();
|
||||
assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (FILE_BITS + 0o400));
|
||||
|
||||
fs.set_mode(&path, 0o400).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::metadata(&path).unwrap().permissions().mode(),
|
||||
FILE_BITS + 0o400
|
||||
);
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_mode_should_update_the_directory_mode_at_path() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
let path = format!("{test_dir}/mydir");
|
||||
std::fs::create_dir(&path).unwrap();
|
||||
assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (DIR_BITS + 0o700));
|
||||
|
||||
fs.set_mode(&path, 0o700).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::metadata(&path).unwrap().permissions().mode(),
|
||||
DIR_BITS + 0o700
|
||||
);
|
||||
teardown(test_dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_mode_should_bubble_up_error_if_some_happens() {
|
||||
let test_dir = setup();
|
||||
let fs = LocalFileSystem;
|
||||
let path = format!("{test_dir}/somemissingfile");
|
||||
// intentionnally don't create file
|
||||
|
||||
let err = fs.set_mode(&path, 0o400).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "No such file or directory (os error 2)");
|
||||
teardown(test_dir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod constants;
|
||||
pub mod fs;
|
||||
pub mod net;
|
||||
pub mod replacer;
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::{io::Cursor, str::FromStr, time::Duration};
|
||||
|
||||
use reqwest::{Method, Request, StatusCode, Url};
|
||||
use tracing::trace;
|
||||
|
||||
use crate::constants::THIS_IS_A_BUG;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
pub async fn download_file(url: String, dest: String) -> Result<()> {
|
||||
let response = reqwest::get(url).await?;
|
||||
let mut file = std::fs::File::create(dest)?;
|
||||
let mut content = Cursor::new(response.bytes().await?);
|
||||
std::io::copy(&mut content, &mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait_ws_ready(url: &str) -> Result<()> {
|
||||
let mut parsed = Url::from_str(url)?;
|
||||
parsed
|
||||
.set_scheme("http")
|
||||
.map_err(|_| anyhow::anyhow!("Can not set the scheme, {THIS_IS_A_BUG}"))?;
|
||||
|
||||
let http_client = reqwest::Client::new();
|
||||
loop {
|
||||
let req = Request::new(Method::OPTIONS, parsed.clone());
|
||||
let res = http_client.execute(req).await;
|
||||
match res {
|
||||
Ok(res) => {
|
||||
if res.status() == StatusCode::OK {
|
||||
// ready to go!
|
||||
break;
|
||||
}
|
||||
|
||||
trace!("http_client status: {}, continuing...", res.status());
|
||||
},
|
||||
Err(e) => {
|
||||
if !skip_err_while_waiting(&e) {
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
trace!("http_client err: {}, continuing... ", e.to_string());
|
||||
},
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn skip_err_while_waiting(e: &reqwest::Error) -> bool {
|
||||
// if the error is connecting/request could be the case that the node
|
||||
// is not listening yet, so we keep waiting
|
||||
// Skipped errs like:
|
||||
// 'tcp connect error: Connection refused (os error 61)'
|
||||
// 'operation was canceled: connection closed before message completed'
|
||||
// 'connection error: Connection reset by peer (os error 54)'
|
||||
e.is_connect() || e.is_request()
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use regex::{Captures, Regex};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use crate::constants::{SHOULD_COMPILE, THIS_IS_A_BUG};
|
||||
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"\{\{([a-zA-Z0-9_]*)\}\}"#)
|
||||
.unwrap_or_else(|_| panic!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
|
||||
static ref TOKEN_PLACEHOLDER: Regex = Regex::new(r#"\{\{ZOMBIE:(.*?):(.*?)\}\}"#)
|
||||
.unwrap_or_else(|_| panic!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
|
||||
static ref PLACEHOLDER_COMPAT: HashMap<&'static str, &'static str> = {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("multiAddress", "multiaddr");
|
||||
m.insert("wsUri", "ws_uri");
|
||||
m.insert("prometheusUri", "prometheus_uri");
|
||||
|
||||
m
|
||||
};
|
||||
}
|
||||
|
||||
/// Return true if the text contains any TOKEN_PLACEHOLDER
|
||||
pub fn has_tokens(text: &str) -> bool {
|
||||
TOKEN_PLACEHOLDER.is_match(text)
|
||||
}
|
||||
|
||||
pub fn apply_replacements(text: &str, replacements: &HashMap<&str, &str>) -> String {
|
||||
let augmented_text = RE.replace_all(text, |caps: &Captures| {
|
||||
if let Some(replacements_value) = replacements.get(&caps[1]) {
|
||||
replacements_value.to_string()
|
||||
} else {
|
||||
caps[0].to_string()
|
||||
}
|
||||
});
|
||||
|
||||
augmented_text.to_string()
|
||||
}
|
||||
|
||||
pub fn apply_env_replacements(text: &str) -> String {
|
||||
let augmented_text = RE.replace_all(text, |caps: &Captures| {
|
||||
if let Ok(replacements_value) = std::env::var(&caps[1]) {
|
||||
replacements_value
|
||||
} else {
|
||||
caps[0].to_string()
|
||||
}
|
||||
});
|
||||
|
||||
augmented_text.to_string()
|
||||
}
|
||||
|
||||
pub fn apply_running_network_replacements(text: &str, network: &serde_json::Value) -> String {
|
||||
let augmented_text = TOKEN_PLACEHOLDER.replace_all(text, |caps: &Captures| {
|
||||
trace!("appling replacements for caps: {caps:#?}");
|
||||
if let Some(node) = network.get(&caps[1]) {
|
||||
trace!("caps1 {} - node: {node}", &caps[1]);
|
||||
let field = *PLACEHOLDER_COMPAT.get(&caps[2]).unwrap_or(&&caps[2]);
|
||||
if let Some(val) = node.get(field) {
|
||||
trace!("caps2 {} - node: {node}", field);
|
||||
val.as_str().unwrap_or("Invalid string").to_string()
|
||||
} else {
|
||||
warn!(
|
||||
"⚠️ The node with name {} doesn't have the value {} in context",
|
||||
&caps[1], &caps[2]
|
||||
);
|
||||
caps[0].to_string()
|
||||
}
|
||||
} else {
|
||||
warn!("⚠️ No node with name {} in context", &caps[1]);
|
||||
caps[0].to_string()
|
||||
}
|
||||
});
|
||||
|
||||
augmented_text.to_string()
|
||||
}
|
||||
|
||||
pub fn get_tokens_to_replace(text: &str) -> HashSet<String> {
|
||||
let mut tokens = HashSet::new();
|
||||
|
||||
TOKEN_PLACEHOLDER
|
||||
.captures_iter(text)
|
||||
.for_each(|caps: Captures| {
|
||||
tokens.insert(caps[1].to_string());
|
||||
});
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn replace_should_works() {
|
||||
let text = "some {{namespace}}";
|
||||
let mut replacements = HashMap::new();
|
||||
replacements.insert("namespace", "demo-123");
|
||||
let res = apply_replacements(text, &replacements);
|
||||
assert_eq!("some demo-123".to_string(), res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_env_should_works() {
|
||||
let text = "some {{namespace}}";
|
||||
std::env::set_var("namespace", "demo-123");
|
||||
// let mut replacements = HashMap::new();
|
||||
// replacements.insert("namespace", "demo-123");
|
||||
let res = apply_env_replacements(text);
|
||||
assert_eq!("some demo-123".to_string(), res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_multiple_should_works() {
|
||||
let text = r#"some {{namespace}}
|
||||
other is {{other}}"#;
|
||||
let augmented_text = r#"some demo-123
|
||||
other is other-123"#;
|
||||
|
||||
let mut replacements = HashMap::new();
|
||||
replacements.insert("namespace", "demo-123");
|
||||
replacements.insert("other", "other-123");
|
||||
let res = apply_replacements(text, &replacements);
|
||||
assert_eq!(augmented_text, res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_multiple_with_missing_should_works() {
|
||||
let text = r#"some {{namespace}}
|
||||
other is {{other}}"#;
|
||||
let augmented_text = r#"some demo-123
|
||||
other is {{other}}"#;
|
||||
|
||||
let mut replacements = HashMap::new();
|
||||
replacements.insert("namespace", "demo-123");
|
||||
|
||||
let res = apply_replacements(text, &replacements);
|
||||
assert_eq!(augmented_text, res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_without_replacement_should_leave_text_unchanged() {
|
||||
let text = "some {{namespace}}";
|
||||
let mut replacements = HashMap::new();
|
||||
replacements.insert("other", "demo-123");
|
||||
let res = apply_replacements(text, &replacements);
|
||||
assert_eq!(text.to_string(), res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_running_network_should_work() {
|
||||
let network = json!({
|
||||
"alice" : {
|
||||
"multiaddr": "some/demo/127.0.0.1"
|
||||
}
|
||||
});
|
||||
|
||||
let res = apply_running_network_replacements("{{ZOMBIE:alice:multiaddr}}", &network);
|
||||
assert_eq!(res.as_str(), "some/demo/127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_running_network_with_compat_should_work() {
|
||||
let network = json!({
|
||||
"alice" : {
|
||||
"multiaddr": "some/demo/127.0.0.1"
|
||||
}
|
||||
});
|
||||
|
||||
let res = apply_running_network_replacements("{{ZOMBIE:alice:multiAddress}}", &network);
|
||||
assert_eq!(res.as_str(), "some/demo/127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_running_network_with_missing_field_should_not_replace_nothing() {
|
||||
let network = json!({
|
||||
"alice" : {
|
||||
"multiaddr": "some/demo/127.0.0.1"
|
||||
}
|
||||
});
|
||||
|
||||
let res = apply_running_network_replacements("{{ZOMBIE:alice:someField}}", &network);
|
||||
assert_eq!(res.as_str(), "{{ZOMBIE:alice:someField}}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_tokens_to_replace_should_work() {
|
||||
let res = get_tokens_to_replace("{{ZOMBIE:alice:multiaddr}} {{ZOMBIE:bob:multiaddr}}");
|
||||
let mut expected = HashSet::new();
|
||||
expected.insert("alice".to_string());
|
||||
expected.insert("bob".to_string());
|
||||
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user