Compare commits

...

8 commits

Author SHA1 Message Date
6227089a10 build(woodpecker): enable fs and network for build and test
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-04-28 11:34:42 +01:00
9339958996 build(woodpecker): whitespace 2024-04-28 11:34:19 +01:00
8d506131ca build(justfile): add validate dev branch recipe
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-28 11:29:44 +01:00
2e3827e0e0 feat(fs): new fs module to replace filesystem 2024-04-28 11:29:44 +01:00
36070f59cc docs(readme): add details about current status and future plans 2024-04-28 11:20:39 +01:00
8d3f5df378 feat(fs): add new contructors for real and temp 2024-04-28 11:20:37 +01:00
a917b21f50 refactor(fs): rename internal structs
trait *Env => *Like
struct *Env => * (remove suffix)
2024-04-28 08:18:49 +01:00
a65689a51c chore(deps): prevent renovate creating pointless PRs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful
Signed-off-by: kemitix <kemitix@noreply.kemitix.net>
2024-04-27 14:29:48 +01:00
13 changed files with 271 additions and 32 deletions

View file

@ -1,5 +1,4 @@
steps:
update-builder-image:
when:
- event: cron
@ -22,7 +21,7 @@ steps:
branch: [main, next]
settings:
# kxio-woodpecker-todo-checker - read:issue
repository_token: '4acf14f93747e044aa2d1397367741b53f3d4f8f'
repository_token: "4acf14f93747e044aa2d1397367741b53f3d4f8f"
prefix_regex: "(#|//) (TODO|FIXME): "
debug: false
@ -37,9 +36,9 @@ steps:
CARGO_TERM_COLOR: always
commands:
- ls -l /usr/local/cargo/bin/
- cargo fmt --all -- --check
- cargo clippy -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used
- cargo build
- cargo fmt --features "fs,network" --all -- --check
- cargo clippy --features "fs,network" -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used
- cargo build --features "fs,network"
test:
when:
@ -51,7 +50,7 @@ steps:
environment:
CARGO_TERM_COLOR: always
commands:
- cargo test
- cargo test --features "fs,network"
publish_to_crates_io:
when:

View file

@ -31,6 +31,9 @@ thiserror = "1.0"
# fs
tempfile = "3.10"
# error handling
derive_more = { version = "1.0.0-beta.6", features = ["from", "display"] }
[dev-dependencies]
# testing
assert2 = "0.3"

View file

@ -3,3 +3,38 @@
[![status-badge](https://ci.kemitix.net/api/badges/53/status.svg)](https://ci.kemitix.net/repos/53)
Provides injectable Filesystem and Network resources to make code more testable.
### FileSystem
There are two FileSystem implementation: [filesystem] and [fs].
- [filesystem] is the legacy implementation and will be removed in a future version.
- [fs] is the current version and is intended to stand-in for and extend the [std::fs] module from the Standard Library.
#### std::fs alternatives
| To Do | [std::fs] | [kxio::fs::FileSystem] | |
| ----- | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| [ ] | canonicalize | path_canonicalize | Returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved. |
| [ ] | copy | file_copy | Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. |
| [ ] | create_dir | dir_create | Creates a new, empty directory at the provided path |
| [ ] | create_dir_all | dir_create_all | Recursively create a directory and all of its parent components if they are missing. |
| [ ] | hard_link | link_create | Creates a new hard link on the filesystem. |
| [ ] | metadata | path_metadata | Given a path, query the file system to get information about a file, directory, etc. |
| [ ] | read | file_read | Read the entire contents of a file into a bytes vector. |
| [ ] | read_dir | dir_read | Returns an iterator over the entries within a directory. |
| [ ] | read_link | link_read | Reads a symbolic link, returning the file that the link points to. |
| [x] | read_to_string | file_read_to_string | Read the entire contents of a file into a string. |
| [ ] | remove_dir | dir_remove | Removes an empty directory. |
| [ ] | remove_dir_all | dir_remove_all | Removes a directory at this path, after removing all its contents. Use carefully! |
| [ ] | remove_file | file_remove | Removes a file from the filesystem. |
| [ ] | rename | path_rename | Rename a file or directory to a new name, replacing the original file if to already exists. |
| [ ] | set_permissions | path_set_permissions | Changes the permissions found on a file or a directory. |
| [ ] | symlink_metadata | link_metadata | Query the metadata about a file without following symlinks. |
| [x] | write | file_write | Write a slice as the entire contents of a file. |
### Network
The entire [network] module needs to be completly rewritten
It's use is strongly discouraged.
A new [net] module will likely be its replacement.

View file

@ -1,3 +1,8 @@
install-hooks:
@echo "Installing git hooks"
git config core.hooksPath .git-hooks
validate-dev-branch:
git rebase -i origin/main -x 'cargo build --features "fs,network"'
git rebase -i origin/main -x 'cargo test --features "fs,network"'
git rebase -i origin/main -x 'cargo clippy --features "fs,network" -- -D warnings -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used'

View file

@ -1,6 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
"extends": ["config:recommended"],
"packageRules": [
{
"matchManagers": ["cargo"],
"rangeStrategy": "replace"
}
]
}

View file

@ -1,4 +1,4 @@
#![allow(unused)]
#![allow(deprecated)]
use std::{
ops::Deref,
@ -9,22 +9,32 @@ use std::{
use tempfile::{tempdir, TempDir};
use tracing::{debug, info};
pub fn real(cwd: Option<PathBuf>) -> FileSystem {
let cwd = cwd.unwrap_or_default();
FileSystem::Real(RealFileSystem::new(cwd))
}
pub fn temp() -> std::io::Result<FileSystem> {
TempFileSystem::new().map(FileSystem::Temp)
}
#[derive(Clone, Debug)]
#[deprecated(since = "1.1.0", note = "Use [kxio::fs::FileSystem] instead")]
pub enum FileSystem {
Real(RealFileSystemEnv),
Temp(TempFileSystemEnv),
Real(RealFileSystem),
Temp(TempFileSystem),
}
impl FileSystem {
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::real()] instead")]
pub fn new_real(cwd: Option<PathBuf>) -> Self {
let cwd = cwd.unwrap_or_default();
Self::Real(RealFileSystemEnv::new(cwd))
real(cwd)
}
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::temp()] instead")]
pub fn new_temp() -> std::io::Result<Self> {
TempFileSystemEnv::new().map(Self::Temp)
temp()
}
}
impl Deref for FileSystem {
type Target = dyn FileSystemEnv;
type Target = dyn FileSystemLike;
fn deref(&self) -> &Self::Target {
match self {
@ -34,7 +44,7 @@ impl Deref for FileSystem {
}
}
pub trait FileSystemEnv: Sync + Send + std::fmt::Debug {
pub trait FileSystemLike: Sync + Send + std::fmt::Debug {
fn cwd(&self) -> &PathBuf;
fn in_cwd(&self, name: &str) -> PathBuf {
@ -72,41 +82,47 @@ pub trait FileSystemEnv: Sync + Send + std::fmt::Debug {
}
#[derive(Clone, Debug, Default)]
pub struct RealFileSystemEnv {
pub struct RealFileSystem {
cwd: PathBuf,
}
#[derive(Clone, Debug)]
pub struct TempFileSystemEnv {
pub struct TempFileSystem {
cwd: PathBuf,
temp_dir: Arc<Mutex<TempDir>>,
// Handle to the temporary directory
// When this handle is dropped the directory is deleted
_temp_dir: Arc<Mutex<TempDir>>,
}
impl FileSystemEnv for TempFileSystemEnv {
impl FileSystemLike for TempFileSystem {
fn cwd(&self) -> &PathBuf {
&self.cwd
}
}
impl FileSystemEnv for RealFileSystemEnv {
impl FileSystemLike for RealFileSystem {
fn cwd(&self) -> &PathBuf {
&self.cwd
}
}
impl RealFileSystemEnv {
impl RealFileSystem {
const fn new(cwd: PathBuf) -> Self {
Self { cwd }
}
}
impl TempFileSystemEnv {
impl TempFileSystem {
fn new() -> std::io::Result<Self> {
let temp_dir = tempdir()?;
info!("temp dir: {:?}", temp_dir.path());
let cwd = temp_dir.path().to_path_buf();
let temp_dir = Arc::new(Mutex::new(temp_dir));
Ok(Self { cwd, temp_dir })
Ok(Self {
cwd,
_temp_dir: temp_dir,
})
}
}
@ -120,13 +136,13 @@ mod tests {
#[test_log::test]
fn test_cwd() {
let cwd = PathBuf::from("/tmp");
let env = RealFileSystemEnv::new(cwd.clone());
let env = RealFileSystem::new(cwd.clone());
assert_eq!(env.cwd(), &cwd);
}
#[test_log::test]
fn test_create_on_temp_fs() -> std::io::Result<()> {
let env = TempFileSystemEnv::new()?;
let env = TempFileSystem::new()?;
assert!(env.cwd().exists());
Ok(())
}
@ -134,13 +150,13 @@ mod tests {
#[test_log::test]
fn test_create_on_real_fs() {
let cwd = PathBuf::from("/tmp");
let env = RealFileSystemEnv::new(cwd.clone());
let env = RealFileSystem::new(cwd.clone());
assert_eq!(env.cwd(), &cwd);
}
#[test_log::test]
fn test_write_and_read_file() -> std::io::Result<()> {
let env = TempFileSystemEnv::new()?;
let env = TempFileSystem::new()?;
let file_name = "test.txt";
let content = "Hello, World!";
let path = env.write_file(file_name, content)?;

50
src/fs/mod.rs Normal file
View file

@ -0,0 +1,50 @@
use std::path::{Path, PathBuf};
use derive_more::From;
mod real;
mod temp;
#[derive(Debug, From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
#[display("Path access attempted outside of base ({base:?}): {path:?}")]
PathTraversal {
base: PathBuf,
path: PathBuf,
},
}
pub type Result<T> = core::result::Result<T, Error>;
pub const fn new(base: PathBuf) -> FileSystem {
FileSystem::Real(real::new(base))
}
pub fn temp() -> Result<FileSystem> {
temp::new().map(FileSystem::Temp)
}
pub enum FileSystem {
Real(real::RealFileSystem),
Temp(temp::TempFileSystem),
}
impl std::ops::Deref for FileSystem {
type Target = dyn FileSystemLike;
fn deref(&self) -> &Self::Target {
match self {
Self::Real(fs) => fs,
Self::Temp(fs) => fs.deref(),
}
}
}
pub trait FileSystemLike {
fn base(&self) -> &Path;
fn path_of(&self, path: PathBuf) -> Result<PathBuf>;
fn file_write(&self, path: &Path, contents: &str) -> Result<()>;
fn file_read_to_string(&self, path: &Path) -> Result<String>;
fn path_exists(&self, path: &Path) -> Result<bool>;
fn path_is_file(&self, path: &Path) -> Result<bool>;
}

52
src/fs/real.rs Normal file
View file

@ -0,0 +1,52 @@
use std::path::{Path, PathBuf};
pub const fn new(base: PathBuf) -> RealFileSystem {
RealFileSystem { base }
}
pub struct RealFileSystem {
base: PathBuf,
}
impl super::FileSystemLike for RealFileSystem {
fn base(&self) -> &Path {
&self.base
}
fn path_of(&self, path: PathBuf) -> super::Result<PathBuf> {
let path_of = self.base.as_path().join(path);
self.validate(&path_of)?;
Ok(path_of)
}
fn file_write(&self, path: &Path, contents: &str) -> super::Result<()> {
self.validate(path)?;
std::fs::write(path, contents).map_err(Into::into)
}
fn file_read_to_string(&self, path: &Path) -> super::Result<String> {
self.validate(path)?;
std::fs::read_to_string(path).map_err(Into::into)
}
fn path_is_file(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.is_file())
}
fn path_exists(&self, path: &Path) -> super::Result<bool> {
self.validate(path)?;
Ok(path.exists())
}
}
impl RealFileSystem {
fn validate(&self, path: &std::path::Path) -> super::Result<()> {
if !path.starts_with(&self.base) {
return Err(super::Error::PathTraversal {
base: self.base.clone(),
path: path.to_path_buf(),
});
}
Ok(())
}
}

28
src/fs/temp.rs Normal file
View file

@ -0,0 +1,28 @@
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
pub(super) fn new() -> super::Result<TempFileSystem> {
let temp_dir = tempfile::tempdir()?;
let base = temp_dir.path().to_path_buf();
let temp_dir = Arc::new(Mutex::new(temp_dir));
let real = super::real::new(base);
Ok(TempFileSystem {
real,
_temp_dir: temp_dir,
})
}
pub struct TempFileSystem {
real: super::real::RealFileSystem,
_temp_dir: Arc<Mutex<TempDir>>,
}
impl std::ops::Deref for TempFileSystem {
type Target = dyn super::FileSystemLike;
fn deref(&self) -> &Self::Target {
&self.real
}
}

View file

@ -1,4 +1,11 @@
#[cfg(feature = "fs")]
pub mod filesystem;
#[cfg(feature = "fs")]
pub mod fs;
#[cfg(feature = "network")]
pub mod network;
#[cfg(test)]
mod tests;

12
src/tests/filesystem.rs Normal file
View file

@ -0,0 +1,12 @@
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn write_read_file_exists() -> TestResult {
let fs = crate::filesystem::temp()?;
let pathbuf = fs.write_file("foo", "content")?;
let c = fs.read_file("foo")?;
assert_eq!(c, "content");
assert!(fs.file_exists(&pathbuf));
Ok(())
}

20
src/tests/fs.rs Normal file
View file

@ -0,0 +1,20 @@
use std::path::PathBuf;
use crate::fs;
type TestResult = Result<(), crate::fs::Error>;
#[test]
fn write_read_file_exists() -> TestResult {
let temp_fs = fs::temp()?;
let name: PathBuf = temp_fs.path_of("foo".into())?;
temp_fs.file_write(&name, "content")?;
let c = temp_fs.file_read_to_string(&name)?;
assert_eq!(c, "content");
let exists = temp_fs.path_exists(&name)?;
assert!(exists);
let is_file = temp_fs.path_is_file(&name)?;
assert!(is_file);
Ok(())
}

8
src/tests/mod.rs Normal file
View file

@ -0,0 +1,8 @@
#[cfg(feature = "fs")]
pub mod filesystem;
#[cfg(feature = "fs")]
pub mod fs;
// #[cfg(feature = "network")]
// pub mod network;