From 9586e5c30da23dd25d404d075a64161689f8cc02 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Sun, 26 May 2024 16:21:48 +0200 Subject: refactor(tvix/castore): move src/fs/test into fuse mod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These tests only interact with the FUSE layer, and import super::fuse to do its work. However, this only works if the `fuse` feature is enabled, which we don't do if we enable the `virtiofs` feature only, causing the tests to fail: ``` ❯ cargo test --no-default-features --features virtiofs Compiling tvix-castore v0.1.0 (/home/flokli/dev/nixos/code.tvl.fyi-submit2/tvix/castore) error[E0432]: unresolved import `super::fuse` --> castore/src/fs/tests.rs:14:13 | 14 | use super::{fuse::FuseDaemon, TvixStoreFs}; | ^^^^ could not find `fuse` in `super` ``` We move src/fs/tests.rs to src/fs/fuse/tests.rs (and src/fs/fuse.rs to src/fs/fuse/mod.rs) to better structure this, which will automatically cause both tests and code to only be built if we have the `fuse` feature enabled. Change-Id: I8fbbad3e4457e326bdfd171aa5c43d25d3187b5b Reviewed-on: https://cl.tvl.fyi/c/depot/+/11715 Autosubmit: flokli Tested-by: BuildkiteCI Reviewed-by: tazjin --- tvix/castore/src/fs/fuse.rs | 120 ---- tvix/castore/src/fs/fuse/mod.rs | 123 ++++ tvix/castore/src/fs/fuse/tests.rs | 1245 +++++++++++++++++++++++++++++++++++++ tvix/castore/src/fs/mod.rs | 3 - tvix/castore/src/fs/tests.rs | 1244 ------------------------------------ 5 files changed, 1368 insertions(+), 1367 deletions(-) delete mode 100644 tvix/castore/src/fs/fuse.rs create mode 100644 tvix/castore/src/fs/fuse/mod.rs create mode 100644 tvix/castore/src/fs/fuse/tests.rs delete mode 100644 tvix/castore/src/fs/tests.rs diff --git a/tvix/castore/src/fs/fuse.rs b/tvix/castore/src/fs/fuse.rs deleted file mode 100644 index cd50618ff5bc..000000000000 --- a/tvix/castore/src/fs/fuse.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::{io, path::Path, sync::Arc, thread}; - -use fuse_backend_rs::{api::filesystem::FileSystem, transport::FuseSession}; -use tracing::{error, instrument}; - -struct FuseServer -where - FS: FileSystem + Sync + Send, -{ - server: Arc>>, - channel: fuse_backend_rs::transport::FuseChannel, -} - -#[cfg(target_os = "macos")] -const BADFD: libc::c_int = libc::EBADF; -#[cfg(target_os = "linux")] -const BADFD: libc::c_int = libc::EBADFD; - -impl FuseServer -where - FS: FileSystem + Sync + Send, -{ - fn start(&mut self) -> io::Result<()> { - while let Some((reader, writer)) = self - .channel - .get_request() - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))? - { - if let Err(e) = self - .server - .handle_message(reader, writer.into(), None, None) - { - match e { - // This indicates the session has been shut down. - fuse_backend_rs::Error::EncodeMessage(e) if e.raw_os_error() == Some(BADFD) => { - break; - } - error => { - error!(?error, "failed to handle fuse request"); - continue; - } - } - } - } - Ok(()) - } -} - -pub struct FuseDaemon { - session: FuseSession, - threads: Vec>, -} - -impl FuseDaemon { - #[instrument(skip(fs, mountpoint), fields(mountpoint=?mountpoint), err)] - pub fn new( - fs: FS, - mountpoint: P, - threads: usize, - allow_other: bool, - ) -> Result - where - FS: FileSystem + Sync + Send + 'static, - P: AsRef + std::fmt::Debug, - { - let server = Arc::new(fuse_backend_rs::api::server::Server::new(Arc::new(fs))); - - let mut session = FuseSession::new(mountpoint.as_ref(), "tvix-store", "", true) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - - #[cfg(target_os = "linux")] - session.set_allow_other(allow_other); - session - .mount() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - let mut join_handles = Vec::with_capacity(threads); - for _ in 0..threads { - let mut server = FuseServer { - server: server.clone(), - channel: session - .new_channel() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?, - }; - let join_handle = thread::Builder::new() - .name("fuse_server".to_string()) - .spawn(move || { - let _ = server.start(); - })?; - join_handles.push(join_handle); - } - - Ok(FuseDaemon { - session, - threads: join_handles, - }) - } - - #[instrument(skip_all, err)] - pub fn unmount(&mut self) -> Result<(), io::Error> { - self.session - .umount() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - - for thread in self.threads.drain(..) { - thread.join().map_err(|_| { - io::Error::new(io::ErrorKind::Other, "failed to join fuse server thread") - })?; - } - - Ok(()) - } -} - -impl Drop for FuseDaemon { - fn drop(&mut self) { - if let Err(error) = self.unmount() { - error!(?error, "failed to unmont fuse filesystem") - } - } -} diff --git a/tvix/castore/src/fs/fuse/mod.rs b/tvix/castore/src/fs/fuse/mod.rs new file mode 100644 index 000000000000..94b73d422a14 --- /dev/null +++ b/tvix/castore/src/fs/fuse/mod.rs @@ -0,0 +1,123 @@ +use std::{io, path::Path, sync::Arc, thread}; + +use fuse_backend_rs::{api::filesystem::FileSystem, transport::FuseSession}; +use tracing::{error, instrument}; + +#[cfg(test)] +mod tests; + +struct FuseServer +where + FS: FileSystem + Sync + Send, +{ + server: Arc>>, + channel: fuse_backend_rs::transport::FuseChannel, +} + +#[cfg(target_os = "macos")] +const BADFD: libc::c_int = libc::EBADF; +#[cfg(target_os = "linux")] +const BADFD: libc::c_int = libc::EBADFD; + +impl FuseServer +where + FS: FileSystem + Sync + Send, +{ + fn start(&mut self) -> io::Result<()> { + while let Some((reader, writer)) = self + .channel + .get_request() + .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))? + { + if let Err(e) = self + .server + .handle_message(reader, writer.into(), None, None) + { + match e { + // This indicates the session has been shut down. + fuse_backend_rs::Error::EncodeMessage(e) if e.raw_os_error() == Some(BADFD) => { + break; + } + error => { + error!(?error, "failed to handle fuse request"); + continue; + } + } + } + } + Ok(()) + } +} + +pub struct FuseDaemon { + session: FuseSession, + threads: Vec>, +} + +impl FuseDaemon { + #[instrument(skip(fs, mountpoint), fields(mountpoint=?mountpoint), err)] + pub fn new( + fs: FS, + mountpoint: P, + threads: usize, + allow_other: bool, + ) -> Result + where + FS: FileSystem + Sync + Send + 'static, + P: AsRef + std::fmt::Debug, + { + let server = Arc::new(fuse_backend_rs::api::server::Server::new(Arc::new(fs))); + + let mut session = FuseSession::new(mountpoint.as_ref(), "tvix-store", "", true) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + #[cfg(target_os = "linux")] + session.set_allow_other(allow_other); + session + .mount() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let mut join_handles = Vec::with_capacity(threads); + for _ in 0..threads { + let mut server = FuseServer { + server: server.clone(), + channel: session + .new_channel() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?, + }; + let join_handle = thread::Builder::new() + .name("fuse_server".to_string()) + .spawn(move || { + let _ = server.start(); + })?; + join_handles.push(join_handle); + } + + Ok(FuseDaemon { + session, + threads: join_handles, + }) + } + + #[instrument(skip_all, err)] + pub fn unmount(&mut self) -> Result<(), io::Error> { + self.session + .umount() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + for thread in self.threads.drain(..) { + thread.join().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "failed to join fuse server thread") + })?; + } + + Ok(()) + } +} + +impl Drop for FuseDaemon { + fn drop(&mut self) { + if let Err(error) = self.unmount() { + error!(?error, "failed to unmont fuse filesystem") + } + } +} diff --git a/tvix/castore/src/fs/fuse/tests.rs b/tvix/castore/src/fs/fuse/tests.rs new file mode 100644 index 000000000000..bb321f5888f8 --- /dev/null +++ b/tvix/castore/src/fs/fuse/tests.rs @@ -0,0 +1,1245 @@ +use bstr::ByteSlice; +use bytes::Bytes; +use std::{ + collections::BTreeMap, + ffi::{OsStr, OsString}, + io::{self, Cursor}, + os::unix::{ffi::OsStrExt, fs::MetadataExt}, + path::Path, + sync::Arc, +}; +use tempfile::TempDir; +use tokio_stream::{wrappers::ReadDirStream, StreamExt}; + +use super::FuseDaemon; +use crate::fs::{TvixStoreFs, XATTR_NAME_BLOB_DIGEST, XATTR_NAME_DIRECTORY_DIGEST}; +use crate::proto as castorepb; +use crate::proto::node::Node; +use crate::{ + blobservice::{BlobService, MemoryBlobService}, + directoryservice::{DirectoryService, MemoryDirectoryService}, + fixtures, +}; + +const BLOB_A_NAME: &str = "00000000000000000000000000000000-test"; +const BLOB_B_NAME: &str = "55555555555555555555555555555555-test"; +const HELLOWORLD_BLOB_NAME: &str = "66666666666666666666666666666666-test"; +const SYMLINK_NAME: &str = "11111111111111111111111111111111-test"; +const SYMLINK_NAME2: &str = "44444444444444444444444444444444-test"; +const DIRECTORY_WITH_KEEP_NAME: &str = "22222222222222222222222222222222-test"; +const DIRECTORY_COMPLICATED_NAME: &str = "33333333333333333333333333333333-test"; + +fn gen_svcs() -> (Arc, Arc) { + ( + Arc::new(MemoryBlobService::default()) as Arc, + Arc::new(MemoryDirectoryService::default()) as Arc, + ) +} + +fn do_mount, BS, DS>( + blob_service: BS, + directory_service: DS, + root_nodes: BTreeMap, + mountpoint: P, + list_root: bool, + show_xattr: bool, +) -> io::Result +where + BS: AsRef + Send + Sync + Clone + 'static, + DS: AsRef + Send + Sync + Clone + 'static, +{ + let fs = TvixStoreFs::new( + blob_service, + directory_service, + Arc::new(root_nodes), + list_root, + show_xattr, + ); + FuseDaemon::new(Arc::new(fs), mountpoint.as_ref(), 4, false) +} + +async fn populate_blob_a( + blob_service: &Arc, + root_nodes: &mut BTreeMap, +) { + let mut bw = blob_service.open_write().await; + tokio::io::copy(&mut Cursor::new(fixtures::BLOB_A.to_vec()), &mut bw) + .await + .expect("must succeed uploading"); + bw.close().await.expect("must succeed closing"); + + root_nodes.insert( + BLOB_A_NAME.into(), + Node::File(castorepb::FileNode { + name: BLOB_A_NAME.into(), + digest: fixtures::BLOB_A_DIGEST.clone().into(), + size: fixtures::BLOB_A.len() as u64, + executable: false, + }), + ); +} + +async fn populate_blob_b( + blob_service: &Arc, + root_nodes: &mut BTreeMap, +) { + let mut bw = blob_service.open_write().await; + tokio::io::copy(&mut Cursor::new(fixtures::BLOB_B.to_vec()), &mut bw) + .await + .expect("must succeed uploading"); + bw.close().await.expect("must succeed closing"); + + root_nodes.insert( + BLOB_B_NAME.into(), + Node::File(castorepb::FileNode { + name: BLOB_B_NAME.into(), + digest: fixtures::BLOB_B_DIGEST.clone().into(), + size: fixtures::BLOB_B.len() as u64, + executable: false, + }), + ); +} + +/// adds a blob containing helloworld and marks it as executable +async fn populate_blob_helloworld( + blob_service: &Arc, + root_nodes: &mut BTreeMap, +) { + let mut bw = blob_service.open_write().await; + tokio::io::copy( + &mut Cursor::new(fixtures::HELLOWORLD_BLOB_CONTENTS.to_vec()), + &mut bw, + ) + .await + .expect("must succeed uploading"); + bw.close().await.expect("must succeed closing"); + + root_nodes.insert( + HELLOWORLD_BLOB_NAME.into(), + Node::File(castorepb::FileNode { + name: HELLOWORLD_BLOB_NAME.into(), + digest: fixtures::HELLOWORLD_BLOB_DIGEST.clone().into(), + size: fixtures::HELLOWORLD_BLOB_CONTENTS.len() as u64, + executable: true, + }), + ); +} + +async fn populate_symlink(root_nodes: &mut BTreeMap) { + root_nodes.insert( + SYMLINK_NAME.into(), + Node::Symlink(castorepb::SymlinkNode { + name: SYMLINK_NAME.into(), + target: BLOB_A_NAME.into(), + }), + ); +} + +/// This writes a symlink pointing to /nix/store/somewhereelse, +/// which is the same symlink target as "aa" inside DIRECTORY_COMPLICATED. +async fn populate_symlink2(root_nodes: &mut BTreeMap) { + root_nodes.insert( + SYMLINK_NAME2.into(), + Node::Symlink(castorepb::SymlinkNode { + name: SYMLINK_NAME2.into(), + target: "/nix/store/somewhereelse".into(), + }), + ); +} + +async fn populate_directory_with_keep( + blob_service: &Arc, + directory_service: &Arc, + root_nodes: &mut BTreeMap, +) { + // upload empty blob + let mut bw = blob_service.open_write().await; + assert_eq!( + fixtures::EMPTY_BLOB_DIGEST.as_slice(), + bw.close().await.expect("must succeed closing").as_slice(), + ); + + // upload directory + directory_service + .put(fixtures::DIRECTORY_WITH_KEEP.clone()) + .await + .expect("must succeed uploading"); + + root_nodes.insert( + DIRECTORY_WITH_KEEP_NAME.into(), + castorepb::node::Node::Directory(castorepb::DirectoryNode { + name: DIRECTORY_WITH_KEEP_NAME.into(), + digest: fixtures::DIRECTORY_WITH_KEEP.digest().into(), + size: fixtures::DIRECTORY_WITH_KEEP.size(), + }), + ); +} + +/// Create a root node for DIRECTORY_WITH_KEEP, but don't upload the Directory +/// itself. +async fn populate_directorynode_without_directory(root_nodes: &mut BTreeMap) { + root_nodes.insert( + DIRECTORY_WITH_KEEP_NAME.into(), + castorepb::node::Node::Directory(castorepb::DirectoryNode { + name: DIRECTORY_WITH_KEEP_NAME.into(), + digest: fixtures::DIRECTORY_WITH_KEEP.digest().into(), + size: fixtures::DIRECTORY_WITH_KEEP.size(), + }), + ); +} + +/// Insert BLOB_A, but don't provide the blob .keep is pointing to. +async fn populate_filenode_without_blob(root_nodes: &mut BTreeMap) { + root_nodes.insert( + BLOB_A_NAME.into(), + Node::File(castorepb::FileNode { + name: BLOB_A_NAME.into(), + digest: fixtures::BLOB_A_DIGEST.clone().into(), + size: fixtures::BLOB_A.len() as u64, + executable: false, + }), + ); +} + +async fn populate_directory_complicated( + blob_service: &Arc, + directory_service: &Arc, + root_nodes: &mut BTreeMap, +) { + // upload empty blob + let mut bw = blob_service.open_write().await; + assert_eq!( + fixtures::EMPTY_BLOB_DIGEST.as_slice(), + bw.close().await.expect("must succeed closing").as_slice(), + ); + + // upload inner directory + directory_service + .put(fixtures::DIRECTORY_WITH_KEEP.clone()) + .await + .expect("must succeed uploading"); + + // upload parent directory + directory_service + .put(fixtures::DIRECTORY_COMPLICATED.clone()) + .await + .expect("must succeed uploading"); + + root_nodes.insert( + DIRECTORY_COMPLICATED_NAME.into(), + Node::Directory(castorepb::DirectoryNode { + name: DIRECTORY_COMPLICATED_NAME.into(), + digest: fixtures::DIRECTORY_COMPLICATED.digest().into(), + size: fixtures::DIRECTORY_COMPLICATED.size(), + }), + ); +} + +/// Ensure mounting itself doesn't fail +#[tokio::test] +async fn mount() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + BTreeMap::default(), + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + fuse_daemon.unmount().expect("unmount"); +} +/// Ensure listing the root isn't allowed +#[tokio::test] +async fn root() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + BTreeMap::default(), + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + { + // read_dir fails (as opendir fails). + let err = tokio::fs::read_dir(tmpdir).await.expect_err("must fail"); + assert_eq!(std::io::ErrorKind::PermissionDenied, err.kind()); + } + + fuse_daemon.unmount().expect("unmount"); +} + +/// Ensure listing the root is allowed if configured explicitly +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn root_with_listing() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_blob_a(&blob_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + true, /* allow listing */ + false, + ) + .expect("must succeed"); + + { + // read_dir succeeds, but getting the first element will fail. + let mut it = ReadDirStream::new(tokio::fs::read_dir(tmpdir).await.expect("must succeed")); + + let e = it + .next() + .await + .expect("must be some") + .expect("must succeed"); + + let metadata = e.metadata().await.expect("must succeed"); + assert!(metadata.is_file()); + assert!(metadata.permissions().readonly()); + assert_eq!(fixtures::BLOB_A.len() as u64, metadata.len()); + } + + fuse_daemon.unmount().expect("unmount"); +} + +/// Ensure we can stat a file at the root +#[tokio::test] +async fn stat_file_at_root() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_blob_a(&blob_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(BLOB_A_NAME); + + // peek at the file metadata + let metadata = tokio::fs::metadata(p).await.expect("must succeed"); + + assert!(metadata.is_file()); + assert!(metadata.permissions().readonly()); + assert_eq!(fixtures::BLOB_A.len() as u64, metadata.len()); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Ensure we can read a file at the root +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn read_file_at_root() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_blob_a(&blob_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(BLOB_A_NAME); + + // read the file contents + let data = tokio::fs::read(p).await.expect("must succeed"); + + // ensure size and contents match + assert_eq!(fixtures::BLOB_A.len(), data.len()); + assert_eq!(fixtures::BLOB_A.to_vec(), data); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Ensure we can read a large file at the root +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn read_large_file_at_root() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_blob_b(&blob_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(BLOB_B_NAME); + { + // peek at the file metadata + let metadata = tokio::fs::metadata(&p).await.expect("must succeed"); + + assert!(metadata.is_file()); + assert!(metadata.permissions().readonly()); + assert_eq!(fixtures::BLOB_B.len() as u64, metadata.len()); + } + + // read the file contents + let data = tokio::fs::read(p).await.expect("must succeed"); + + // ensure size and contents match + assert_eq!(fixtures::BLOB_B.len(), data.len()); + assert_eq!(fixtures::BLOB_B.to_vec(), data); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Read the target of a symlink +#[tokio::test] +async fn symlink_readlink() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_symlink(&mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(SYMLINK_NAME); + + let target = tokio::fs::read_link(&p).await.expect("must succeed"); + assert_eq!(BLOB_A_NAME, target.to_str().unwrap()); + + // peek at the file metadata, which follows symlinks. + // this must fail, as we didn't populate the target. + let e = tokio::fs::metadata(&p).await.expect_err("must fail"); + assert_eq!(std::io::ErrorKind::NotFound, e.kind()); + + // peeking at the file metadata without following symlinks will succeed. + let metadata = tokio::fs::symlink_metadata(&p).await.expect("must succeed"); + assert!(metadata.is_symlink()); + + // reading from the symlink (which follows) will fail, because the target doesn't exist. + let e = tokio::fs::read(p).await.expect_err("must fail"); + assert_eq!(std::io::ErrorKind::NotFound, e.kind()); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Read and stat a regular file through a symlink pointing to it. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn read_stat_through_symlink() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_blob_a(&blob_service, &mut root_nodes).await; + populate_symlink(&mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p_symlink = tmpdir.path().join(SYMLINK_NAME); + let p_blob = tmpdir.path().join(SYMLINK_NAME); + + // peek at the file metadata, which follows symlinks. + // this must now return the same metadata as when statting at the target directly. + let metadata_symlink = tokio::fs::metadata(&p_symlink).await.expect("must succeed"); + let metadata_blob = tokio::fs::metadata(&p_blob).await.expect("must succeed"); + assert_eq!(metadata_blob.file_type(), metadata_symlink.file_type()); + assert_eq!(metadata_blob.len(), metadata_symlink.len()); + + // reading from the symlink (which follows) will return the same data as if + // we were reading from the file directly. + assert_eq!( + tokio::fs::read(p_blob).await.expect("must succeed"), + tokio::fs::read(p_symlink).await.expect("must succeed"), + ); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Read a directory in the root, and validate some attributes. +#[tokio::test] +async fn read_stat_directory() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); + + // peek at the metadata of the directory + let metadata = tokio::fs::metadata(p).await.expect("must succeed"); + assert!(metadata.is_dir()); + assert!(metadata.permissions().readonly()); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Read a directory and file in the root, and ensure the xattrs expose blob or +/// directory digests. +#[tokio::test] +async fn xattr() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; + populate_blob_a(&blob_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + true, /* support xattr */ + ) + .expect("must succeed"); + + // peek at the directory + { + let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); + + let xattr_names: Vec = xattr::list(&p).expect("must succeed").collect(); + // There should be 1 key, XATTR_NAME_DIRECTORY_DIGEST. + assert_eq!(1, xattr_names.len(), "there should be 1 xattr name"); + assert_eq!( + XATTR_NAME_DIRECTORY_DIGEST, + xattr_names.first().unwrap().as_encoded_bytes() + ); + + // The key should equal to the string-formatted b3 digest. + let val = xattr::get(&p, OsStr::from_bytes(XATTR_NAME_DIRECTORY_DIGEST)) + .expect("must succeed") + .expect("must be some"); + assert_eq!( + fixtures::DIRECTORY_WITH_KEEP + .digest() + .to_string() + .as_bytes() + .as_bstr(), + val.as_bstr() + ); + + // Reading another xattr key is gonna return None. + let val = xattr::get(&p, OsStr::from_bytes(b"user.cheesecake")).expect("must succeed"); + assert_eq!(None, val); + } + // peek at the file + { + let p = tmpdir.path().join(BLOB_A_NAME); + + let xattr_names: Vec = xattr::list(&p).expect("must succeed").collect(); + // There should be 1 key, XATTR_NAME_BLOB_DIGEST. + assert_eq!(1, xattr_names.len(), "there should be 1 xattr name"); + assert_eq!( + XATTR_NAME_BLOB_DIGEST, + xattr_names.first().unwrap().as_encoded_bytes() + ); + + // The key should equal to the string-formatted b3 digest. + let val = xattr::get(&p, OsStr::from_bytes(XATTR_NAME_BLOB_DIGEST)) + .expect("must succeed") + .expect("must be some"); + assert_eq!( + fixtures::BLOB_A_DIGEST.to_string().as_bytes().as_bstr(), + val.as_bstr() + ); + + // Reading another xattr key is gonna return None. + let val = xattr::get(&p, OsStr::from_bytes(b"user.cheesecake")).expect("must succeed"); + assert_eq!(None, val); + } + + fuse_daemon.unmount().expect("unmount"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +/// Read a blob inside a directory. This ensures we successfully populate directory data. +async fn read_blob_inside_dir() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME).join(".keep"); + + // peek at metadata. + let metadata = tokio::fs::metadata(&p).await.expect("must succeed"); + assert!(metadata.is_file()); + assert!(metadata.permissions().readonly()); + + // read from it + let data = tokio::fs::read(&p).await.expect("must succeed"); + assert_eq!(fixtures::EMPTY_BLOB_CONTENTS.to_vec(), data); + + fuse_daemon.unmount().expect("unmount"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +/// Read a blob inside a directory inside a directory. This ensures we properly +/// populate directories as we traverse down the structure. +async fn read_blob_deep_inside_dir() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir + .path() + .join(DIRECTORY_COMPLICATED_NAME) + .join("keep") + .join(".keep"); + + // peek at metadata. + let metadata = tokio::fs::metadata(&p).await.expect("must succeed"); + assert!(metadata.is_file()); + assert!(metadata.permissions().readonly()); + + // read from it + let data = tokio::fs::read(&p).await.expect("must succeed"); + assert_eq!(fixtures::EMPTY_BLOB_CONTENTS.to_vec(), data); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Ensure readdir works. +#[tokio::test] +async fn readdir() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME); + + { + // read_dir should succeed. Collect all elements + let elements: Vec<_> = + ReadDirStream::new(tokio::fs::read_dir(p).await.expect("must succeed")) + .map(|e| e.expect("must not be err")) + .collect() + .await; + + assert_eq!(3, elements.len(), "number of elements should be 3"); // rust skips . and .. + + // We explicitly look at specific positions here, because we always emit + // them ordered. + + // ".keep", 0 byte file. + let e = &elements[0]; + assert_eq!(".keep", e.file_name()); + assert!(e.file_type().await.expect("must succeed").is_file()); + assert_eq!(0, e.metadata().await.expect("must succeed").len()); + + // "aa", symlink. + let e = &elements[1]; + assert_eq!("aa", e.file_name()); + assert!(e.file_type().await.expect("must succeed").is_symlink()); + + // "keep", directory + let e = &elements[2]; + assert_eq!("keep", e.file_name()); + assert!(e.file_type().await.expect("must succeed").is_dir()); + } + + fuse_daemon.unmount().expect("unmount"); +} + +#[tokio::test] +/// Do a readdir deeper inside a directory, without doing readdir or stat in the parent directory. +async fn readdir_deep() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join("keep"); + + { + // read_dir should succeed. Collect all elements + let elements: Vec<_> = + ReadDirStream::new(tokio::fs::read_dir(p).await.expect("must succeed")) + .map(|e| e.expect("must not be err")) + .collect() + .await; + + assert_eq!(1, elements.len(), "number of elements should be 1"); // rust skips . and .. + + // ".keep", 0 byte file. + let e = &elements[0]; + assert_eq!(".keep", e.file_name()); + assert!(e.file_type().await.expect("must succeed").is_file()); + assert_eq!(0, e.metadata().await.expect("must succeed").len()); + } + + fuse_daemon.unmount().expect("unmount"); +} + +/// Check attributes match how they show up in /nix/store normally. +#[tokio::test] +async fn check_attributes() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_blob_a(&blob_service, &mut root_nodes).await; + populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; + populate_symlink(&mut root_nodes).await; + populate_blob_helloworld(&blob_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p_file = tmpdir.path().join(BLOB_A_NAME); + let p_directory = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); + let p_symlink = tmpdir.path().join(SYMLINK_NAME); + let p_executable_file = tmpdir.path().join(HELLOWORLD_BLOB_NAME); + + // peek at metadata. We use symlink_metadata to ensure we don't traverse a symlink by accident. + let metadata_file = tokio::fs::symlink_metadata(&p_file) + .await + .expect("must succeed"); + let metadata_executable_file = tokio::fs::symlink_metadata(&p_executable_file) + .await + .expect("must succeed"); + let metadata_directory = tokio::fs::symlink_metadata(&p_directory) + .await + .expect("must succeed"); + let metadata_symlink = tokio::fs::symlink_metadata(&p_symlink) + .await + .expect("must succeed"); + + // modes should match. We & with 0o777 to remove any higher bits. + assert_eq!(0o444, metadata_file.mode() & 0o777); + assert_eq!(0o555, metadata_executable_file.mode() & 0o777); + assert_eq!(0o555, metadata_directory.mode() & 0o777); + assert_eq!(0o444, metadata_symlink.mode() & 0o777); + + // files should have the correct filesize + assert_eq!(fixtures::BLOB_A.len() as u64, metadata_file.len()); + // directories should have their "size" as filesize + assert_eq!( + { fixtures::DIRECTORY_WITH_KEEP.size() }, + metadata_directory.size() + ); + + for metadata in &[&metadata_file, &metadata_directory, &metadata_symlink] { + // uid and gid should be 0. + assert_eq!(0, metadata.uid()); + assert_eq!(0, metadata.gid()); + + // all times should be set to the unix epoch. + assert_eq!(0, metadata.atime()); + assert_eq!(0, metadata.mtime()); + assert_eq!(0, metadata.ctime()); + // crtime seems MacOS only + } + + fuse_daemon.unmount().expect("unmount"); +} + +#[tokio::test] +/// Ensure we allocate the same inodes for the same directory contents. +/// $DIRECTORY_COMPLICATED_NAME/keep contains the same data as $DIRECTORY_WITH_KEEP. +async fn compare_inodes_directories() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; + populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p_dir_with_keep = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); + let p_sibling_dir = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join("keep"); + + // peek at metadata. + assert_eq!( + tokio::fs::metadata(p_dir_with_keep) + .await + .expect("must succeed") + .ino(), + tokio::fs::metadata(p_sibling_dir) + .await + .expect("must succeed") + .ino() + ); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Ensure we allocate the same inodes for the same directory contents. +/// $DIRECTORY_COMPLICATED_NAME/keep/,keep contains the same data as $DIRECTORY_COMPLICATED_NAME/.keep +#[tokio::test] +async fn compare_inodes_files() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p_keep1 = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join(".keep"); + let p_keep2 = tmpdir + .path() + .join(DIRECTORY_COMPLICATED_NAME) + .join("keep") + .join(".keep"); + + // peek at metadata. + assert_eq!( + tokio::fs::metadata(p_keep1) + .await + .expect("must succeed") + .ino(), + tokio::fs::metadata(p_keep2) + .await + .expect("must succeed") + .ino() + ); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Ensure we allocate the same inode for symlinks pointing to the same targets. +/// $DIRECTORY_COMPLICATED_NAME/aa points to the same target as SYMLINK_NAME2. +#[tokio::test] +async fn compare_inodes_symlinks() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; + populate_symlink2(&mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p1 = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join("aa"); + let p2 = tmpdir.path().join(SYMLINK_NAME2); + + // peek at metadata. + assert_eq!( + tokio::fs::symlink_metadata(p1) + .await + .expect("must succeed") + .ino(), + tokio::fs::symlink_metadata(p2) + .await + .expect("must succeed") + .ino() + ); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Check we match paths exactly. +#[tokio::test] +async fn read_wrong_paths_in_root() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_blob_a(&blob_service, &mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + // wrong name + assert!( + tokio::fs::metadata(tmpdir.path().join("00000000000000000000000000000000-tes")) + .await + .is_err() + ); + + // invalid hash + assert!( + tokio::fs::metadata(tmpdir.path().join("0000000000000000000000000000000-test")) + .await + .is_err() + ); + + // right name, must exist + assert!( + tokio::fs::metadata(tmpdir.path().join("00000000000000000000000000000000-test")) + .await + .is_ok() + ); + + // now wrong name with right hash still may not exist + assert!( + tokio::fs::metadata(tmpdir.path().join("00000000000000000000000000000000-tes")) + .await + .is_err() + ); + + fuse_daemon.unmount().expect("unmount"); +} + +/// Make sure writes are not allowed +#[tokio::test] +async fn disallow_writes() { + // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let root_nodes = BTreeMap::default(); + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(BLOB_A_NAME); + let e = tokio::fs::File::create(p).await.expect_err("must fail"); + + assert_eq!(Some(libc::EROFS), e.raw_os_error()); + + fuse_daemon.unmount().expect("unmount"); +} + +#[tokio::test] +/// Ensure we get an IO error if the directory service does not have the Directory object. +async fn missing_directory() { + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_directorynode_without_directory(&mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); + + { + // `stat` on the path should succeed, because it doesn't trigger the directory request. + tokio::fs::metadata(&p).await.expect("must succeed"); + + // However, calling either `readdir` or `stat` on a child should fail with an IO error. + // It fails when trying to pull the first entry, because we don't implement opendir separately + ReadDirStream::new(tokio::fs::read_dir(&p).await.unwrap()) + .next() + .await + .expect("must be some") + .expect_err("must be err"); + + // rust currently sets e.kind() to Uncategorized, which isn't very + // helpful, so we don't look at the error more closely than that.. + tokio::fs::metadata(p.join(".keep")) + .await + .expect_err("must fail"); + } + + fuse_daemon.unmount().expect("unmount"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +/// Ensure we get an IO error if the blob service does not have the blob +async fn missing_blob() { + if !std::path::Path::new("/dev/fuse").exists() { + eprintln!("skipping test"); + return; + } + let tmpdir = TempDir::new().unwrap(); + + let (blob_service, directory_service) = gen_svcs(); + let mut root_nodes = BTreeMap::default(); + + populate_filenode_without_blob(&mut root_nodes).await; + + let mut fuse_daemon = do_mount( + blob_service, + directory_service, + root_nodes, + tmpdir.path(), + false, + false, + ) + .expect("must succeed"); + + let p = tmpdir.path().join(BLOB_A_NAME); + + { + // `stat` on the blob should succeed, because it doesn't trigger a request to the blob service. + tokio::fs::metadata(&p).await.expect("must succeed"); + + // However, calling read on the blob should fail. + // rust currently sets e.kind() to Uncategorized, which isn't very + // helpful, so we don't look at the error more closely than that.. + tokio::fs::read(p).await.expect_err("must fail"); + } + + fuse_daemon.unmount().expect("unmount"); +} diff --git a/tvix/castore/src/fs/mod.rs b/tvix/castore/src/fs/mod.rs index 826523131fbd..176199f64aaf 100644 --- a/tvix/castore/src/fs/mod.rs +++ b/tvix/castore/src/fs/mod.rs @@ -9,9 +9,6 @@ pub mod fuse; #[cfg(feature = "virtiofs")] pub mod virtiofs; -#[cfg(test)] -mod tests; - pub use self::root_nodes::RootNodes; use self::{ file_attr::ROOT_FILE_ATTR, diff --git a/tvix/castore/src/fs/tests.rs b/tvix/castore/src/fs/tests.rs deleted file mode 100644 index d6eeb8a4113d..000000000000 --- a/tvix/castore/src/fs/tests.rs +++ /dev/null @@ -1,1244 +0,0 @@ -use bstr::ByteSlice; -use bytes::Bytes; -use std::{ - collections::BTreeMap, - ffi::{OsStr, OsString}, - io::{self, Cursor}, - os::unix::{ffi::OsStrExt, fs::MetadataExt}, - path::Path, - sync::Arc, -}; -use tempfile::TempDir; -use tokio_stream::{wrappers::ReadDirStream, StreamExt}; - -use super::{fuse::FuseDaemon, TvixStoreFs}; -use crate::proto as castorepb; -use crate::proto::node::Node; -use crate::{ - blobservice::{BlobService, MemoryBlobService}, - directoryservice::{DirectoryService, MemoryDirectoryService}, - fixtures, -}; - -const BLOB_A_NAME: &str = "00000000000000000000000000000000-test"; -const BLOB_B_NAME: &str = "55555555555555555555555555555555-test"; -const HELLOWORLD_BLOB_NAME: &str = "66666666666666666666666666666666-test"; -const SYMLINK_NAME: &str = "11111111111111111111111111111111-test"; -const SYMLINK_NAME2: &str = "44444444444444444444444444444444-test"; -const DIRECTORY_WITH_KEEP_NAME: &str = "22222222222222222222222222222222-test"; -const DIRECTORY_COMPLICATED_NAME: &str = "33333333333333333333333333333333-test"; - -fn gen_svcs() -> (Arc, Arc) { - ( - Arc::new(MemoryBlobService::default()) as Arc, - Arc::new(MemoryDirectoryService::default()) as Arc, - ) -} - -fn do_mount, BS, DS>( - blob_service: BS, - directory_service: DS, - root_nodes: BTreeMap, - mountpoint: P, - list_root: bool, - show_xattr: bool, -) -> io::Result -where - BS: AsRef + Send + Sync + Clone + 'static, - DS: AsRef + Send + Sync + Clone + 'static, -{ - let fs = TvixStoreFs::new( - blob_service, - directory_service, - Arc::new(root_nodes), - list_root, - show_xattr, - ); - FuseDaemon::new(Arc::new(fs), mountpoint.as_ref(), 4, false) -} - -async fn populate_blob_a( - blob_service: &Arc, - root_nodes: &mut BTreeMap, -) { - let mut bw = blob_service.open_write().await; - tokio::io::copy(&mut Cursor::new(fixtures::BLOB_A.to_vec()), &mut bw) - .await - .expect("must succeed uploading"); - bw.close().await.expect("must succeed closing"); - - root_nodes.insert( - BLOB_A_NAME.into(), - Node::File(castorepb::FileNode { - name: BLOB_A_NAME.into(), - digest: fixtures::BLOB_A_DIGEST.clone().into(), - size: fixtures::BLOB_A.len() as u64, - executable: false, - }), - ); -} - -async fn populate_blob_b( - blob_service: &Arc, - root_nodes: &mut BTreeMap, -) { - let mut bw = blob_service.open_write().await; - tokio::io::copy(&mut Cursor::new(fixtures::BLOB_B.to_vec()), &mut bw) - .await - .expect("must succeed uploading"); - bw.close().await.expect("must succeed closing"); - - root_nodes.insert( - BLOB_B_NAME.into(), - Node::File(castorepb::FileNode { - name: BLOB_B_NAME.into(), - digest: fixtures::BLOB_B_DIGEST.clone().into(), - size: fixtures::BLOB_B.len() as u64, - executable: false, - }), - ); -} - -/// adds a blob containing helloworld and marks it as executable -async fn populate_blob_helloworld( - blob_service: &Arc, - root_nodes: &mut BTreeMap, -) { - let mut bw = blob_service.open_write().await; - tokio::io::copy( - &mut Cursor::new(fixtures::HELLOWORLD_BLOB_CONTENTS.to_vec()), - &mut bw, - ) - .await - .expect("must succeed uploading"); - bw.close().await.expect("must succeed closing"); - - root_nodes.insert( - HELLOWORLD_BLOB_NAME.into(), - Node::File(castorepb::FileNode { - name: HELLOWORLD_BLOB_NAME.into(), - digest: fixtures::HELLOWORLD_BLOB_DIGEST.clone().into(), - size: fixtures::HELLOWORLD_BLOB_CONTENTS.len() as u64, - executable: true, - }), - ); -} - -async fn populate_symlink(root_nodes: &mut BTreeMap) { - root_nodes.insert( - SYMLINK_NAME.into(), - Node::Symlink(castorepb::SymlinkNode { - name: SYMLINK_NAME.into(), - target: BLOB_A_NAME.into(), - }), - ); -} - -/// This writes a symlink pointing to /nix/store/somewhereelse, -/// which is the same symlink target as "aa" inside DIRECTORY_COMPLICATED. -async fn populate_symlink2(root_nodes: &mut BTreeMap) { - root_nodes.insert( - SYMLINK_NAME2.into(), - Node::Symlink(castorepb::SymlinkNode { - name: SYMLINK_NAME2.into(), - target: "/nix/store/somewhereelse".into(), - }), - ); -} - -async fn populate_directory_with_keep( - blob_service: &Arc, - directory_service: &Arc, - root_nodes: &mut BTreeMap, -) { - // upload empty blob - let mut bw = blob_service.open_write().await; - assert_eq!( - fixtures::EMPTY_BLOB_DIGEST.as_slice(), - bw.close().await.expect("must succeed closing").as_slice(), - ); - - // upload directory - directory_service - .put(fixtures::DIRECTORY_WITH_KEEP.clone()) - .await - .expect("must succeed uploading"); - - root_nodes.insert( - DIRECTORY_WITH_KEEP_NAME.into(), - castorepb::node::Node::Directory(castorepb::DirectoryNode { - name: DIRECTORY_WITH_KEEP_NAME.into(), - digest: fixtures::DIRECTORY_WITH_KEEP.digest().into(), - size: fixtures::DIRECTORY_WITH_KEEP.size(), - }), - ); -} - -/// Create a root node for DIRECTORY_WITH_KEEP, but don't upload the Directory -/// itself. -async fn populate_directorynode_without_directory(root_nodes: &mut BTreeMap) { - root_nodes.insert( - DIRECTORY_WITH_KEEP_NAME.into(), - castorepb::node::Node::Directory(castorepb::DirectoryNode { - name: DIRECTORY_WITH_KEEP_NAME.into(), - digest: fixtures::DIRECTORY_WITH_KEEP.digest().into(), - size: fixtures::DIRECTORY_WITH_KEEP.size(), - }), - ); -} - -/// Insert BLOB_A, but don't provide the blob .keep is pointing to. -async fn populate_filenode_without_blob(root_nodes: &mut BTreeMap) { - root_nodes.insert( - BLOB_A_NAME.into(), - Node::File(castorepb::FileNode { - name: BLOB_A_NAME.into(), - digest: fixtures::BLOB_A_DIGEST.clone().into(), - size: fixtures::BLOB_A.len() as u64, - executable: false, - }), - ); -} - -async fn populate_directory_complicated( - blob_service: &Arc, - directory_service: &Arc, - root_nodes: &mut BTreeMap, -) { - // upload empty blob - let mut bw = blob_service.open_write().await; - assert_eq!( - fixtures::EMPTY_BLOB_DIGEST.as_slice(), - bw.close().await.expect("must succeed closing").as_slice(), - ); - - // upload inner directory - directory_service - .put(fixtures::DIRECTORY_WITH_KEEP.clone()) - .await - .expect("must succeed uploading"); - - // upload parent directory - directory_service - .put(fixtures::DIRECTORY_COMPLICATED.clone()) - .await - .expect("must succeed uploading"); - - root_nodes.insert( - DIRECTORY_COMPLICATED_NAME.into(), - Node::Directory(castorepb::DirectoryNode { - name: DIRECTORY_COMPLICATED_NAME.into(), - digest: fixtures::DIRECTORY_COMPLICATED.digest().into(), - size: fixtures::DIRECTORY_COMPLICATED.size(), - }), - ); -} - -/// Ensure mounting itself doesn't fail -#[tokio::test] -async fn mount() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - BTreeMap::default(), - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - fuse_daemon.unmount().expect("unmount"); -} -/// Ensure listing the root isn't allowed -#[tokio::test] -async fn root() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - BTreeMap::default(), - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - { - // read_dir fails (as opendir fails). - let err = tokio::fs::read_dir(tmpdir).await.expect_err("must fail"); - assert_eq!(std::io::ErrorKind::PermissionDenied, err.kind()); - } - - fuse_daemon.unmount().expect("unmount"); -} - -/// Ensure listing the root is allowed if configured explicitly -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn root_with_listing() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_blob_a(&blob_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - true, /* allow listing */ - false, - ) - .expect("must succeed"); - - { - // read_dir succeeds, but getting the first element will fail. - let mut it = ReadDirStream::new(tokio::fs::read_dir(tmpdir).await.expect("must succeed")); - - let e = it - .next() - .await - .expect("must be some") - .expect("must succeed"); - - let metadata = e.metadata().await.expect("must succeed"); - assert!(metadata.is_file()); - assert!(metadata.permissions().readonly()); - assert_eq!(fixtures::BLOB_A.len() as u64, metadata.len()); - } - - fuse_daemon.unmount().expect("unmount"); -} - -/// Ensure we can stat a file at the root -#[tokio::test] -async fn stat_file_at_root() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_blob_a(&blob_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(BLOB_A_NAME); - - // peek at the file metadata - let metadata = tokio::fs::metadata(p).await.expect("must succeed"); - - assert!(metadata.is_file()); - assert!(metadata.permissions().readonly()); - assert_eq!(fixtures::BLOB_A.len() as u64, metadata.len()); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Ensure we can read a file at the root -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn read_file_at_root() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_blob_a(&blob_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(BLOB_A_NAME); - - // read the file contents - let data = tokio::fs::read(p).await.expect("must succeed"); - - // ensure size and contents match - assert_eq!(fixtures::BLOB_A.len(), data.len()); - assert_eq!(fixtures::BLOB_A.to_vec(), data); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Ensure we can read a large file at the root -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn read_large_file_at_root() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_blob_b(&blob_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(BLOB_B_NAME); - { - // peek at the file metadata - let metadata = tokio::fs::metadata(&p).await.expect("must succeed"); - - assert!(metadata.is_file()); - assert!(metadata.permissions().readonly()); - assert_eq!(fixtures::BLOB_B.len() as u64, metadata.len()); - } - - // read the file contents - let data = tokio::fs::read(p).await.expect("must succeed"); - - // ensure size and contents match - assert_eq!(fixtures::BLOB_B.len(), data.len()); - assert_eq!(fixtures::BLOB_B.to_vec(), data); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Read the target of a symlink -#[tokio::test] -async fn symlink_readlink() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_symlink(&mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(SYMLINK_NAME); - - let target = tokio::fs::read_link(&p).await.expect("must succeed"); - assert_eq!(BLOB_A_NAME, target.to_str().unwrap()); - - // peek at the file metadata, which follows symlinks. - // this must fail, as we didn't populate the target. - let e = tokio::fs::metadata(&p).await.expect_err("must fail"); - assert_eq!(std::io::ErrorKind::NotFound, e.kind()); - - // peeking at the file metadata without following symlinks will succeed. - let metadata = tokio::fs::symlink_metadata(&p).await.expect("must succeed"); - assert!(metadata.is_symlink()); - - // reading from the symlink (which follows) will fail, because the target doesn't exist. - let e = tokio::fs::read(p).await.expect_err("must fail"); - assert_eq!(std::io::ErrorKind::NotFound, e.kind()); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Read and stat a regular file through a symlink pointing to it. -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn read_stat_through_symlink() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_blob_a(&blob_service, &mut root_nodes).await; - populate_symlink(&mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p_symlink = tmpdir.path().join(SYMLINK_NAME); - let p_blob = tmpdir.path().join(SYMLINK_NAME); - - // peek at the file metadata, which follows symlinks. - // this must now return the same metadata as when statting at the target directly. - let metadata_symlink = tokio::fs::metadata(&p_symlink).await.expect("must succeed"); - let metadata_blob = tokio::fs::metadata(&p_blob).await.expect("must succeed"); - assert_eq!(metadata_blob.file_type(), metadata_symlink.file_type()); - assert_eq!(metadata_blob.len(), metadata_symlink.len()); - - // reading from the symlink (which follows) will return the same data as if - // we were reading from the file directly. - assert_eq!( - tokio::fs::read(p_blob).await.expect("must succeed"), - tokio::fs::read(p_symlink).await.expect("must succeed"), - ); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Read a directory in the root, and validate some attributes. -#[tokio::test] -async fn read_stat_directory() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); - - // peek at the metadata of the directory - let metadata = tokio::fs::metadata(p).await.expect("must succeed"); - assert!(metadata.is_dir()); - assert!(metadata.permissions().readonly()); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Read a directory and file in the root, and ensure the xattrs expose blob or -/// directory digests. -#[tokio::test] -async fn xattr() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; - populate_blob_a(&blob_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - true, /* support xattr */ - ) - .expect("must succeed"); - - // peek at the directory - { - let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); - - let xattr_names: Vec = xattr::list(&p).expect("must succeed").collect(); - // There should be 1 key, XATTR_NAME_DIRECTORY_DIGEST. - assert_eq!(1, xattr_names.len(), "there should be 1 xattr name"); - assert_eq!( - super::XATTR_NAME_DIRECTORY_DIGEST, - xattr_names.first().unwrap().as_encoded_bytes() - ); - - // The key should equal to the string-formatted b3 digest. - let val = xattr::get(&p, OsStr::from_bytes(super::XATTR_NAME_DIRECTORY_DIGEST)) - .expect("must succeed") - .expect("must be some"); - assert_eq!( - fixtures::DIRECTORY_WITH_KEEP - .digest() - .to_string() - .as_bytes() - .as_bstr(), - val.as_bstr() - ); - - // Reading another xattr key is gonna return None. - let val = xattr::get(&p, OsStr::from_bytes(b"user.cheesecake")).expect("must succeed"); - assert_eq!(None, val); - } - // peek at the file - { - let p = tmpdir.path().join(BLOB_A_NAME); - - let xattr_names: Vec = xattr::list(&p).expect("must succeed").collect(); - // There should be 1 key, XATTR_NAME_BLOB_DIGEST. - assert_eq!(1, xattr_names.len(), "there should be 1 xattr name"); - assert_eq!( - super::XATTR_NAME_BLOB_DIGEST, - xattr_names.first().unwrap().as_encoded_bytes() - ); - - // The key should equal to the string-formatted b3 digest. - let val = xattr::get(&p, OsStr::from_bytes(super::XATTR_NAME_BLOB_DIGEST)) - .expect("must succeed") - .expect("must be some"); - assert_eq!( - fixtures::BLOB_A_DIGEST.to_string().as_bytes().as_bstr(), - val.as_bstr() - ); - - // Reading another xattr key is gonna return None. - let val = xattr::get(&p, OsStr::from_bytes(b"user.cheesecake")).expect("must succeed"); - assert_eq!(None, val); - } - - fuse_daemon.unmount().expect("unmount"); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -/// Read a blob inside a directory. This ensures we successfully populate directory data. -async fn read_blob_inside_dir() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME).join(".keep"); - - // peek at metadata. - let metadata = tokio::fs::metadata(&p).await.expect("must succeed"); - assert!(metadata.is_file()); - assert!(metadata.permissions().readonly()); - - // read from it - let data = tokio::fs::read(&p).await.expect("must succeed"); - assert_eq!(fixtures::EMPTY_BLOB_CONTENTS.to_vec(), data); - - fuse_daemon.unmount().expect("unmount"); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -/// Read a blob inside a directory inside a directory. This ensures we properly -/// populate directories as we traverse down the structure. -async fn read_blob_deep_inside_dir() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir - .path() - .join(DIRECTORY_COMPLICATED_NAME) - .join("keep") - .join(".keep"); - - // peek at metadata. - let metadata = tokio::fs::metadata(&p).await.expect("must succeed"); - assert!(metadata.is_file()); - assert!(metadata.permissions().readonly()); - - // read from it - let data = tokio::fs::read(&p).await.expect("must succeed"); - assert_eq!(fixtures::EMPTY_BLOB_CONTENTS.to_vec(), data); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Ensure readdir works. -#[tokio::test] -async fn readdir() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME); - - { - // read_dir should succeed. Collect all elements - let elements: Vec<_> = - ReadDirStream::new(tokio::fs::read_dir(p).await.expect("must succeed")) - .map(|e| e.expect("must not be err")) - .collect() - .await; - - assert_eq!(3, elements.len(), "number of elements should be 3"); // rust skips . and .. - - // We explicitly look at specific positions here, because we always emit - // them ordered. - - // ".keep", 0 byte file. - let e = &elements[0]; - assert_eq!(".keep", e.file_name()); - assert!(e.file_type().await.expect("must succeed").is_file()); - assert_eq!(0, e.metadata().await.expect("must succeed").len()); - - // "aa", symlink. - let e = &elements[1]; - assert_eq!("aa", e.file_name()); - assert!(e.file_type().await.expect("must succeed").is_symlink()); - - // "keep", directory - let e = &elements[2]; - assert_eq!("keep", e.file_name()); - assert!(e.file_type().await.expect("must succeed").is_dir()); - } - - fuse_daemon.unmount().expect("unmount"); -} - -#[tokio::test] -/// Do a readdir deeper inside a directory, without doing readdir or stat in the parent directory. -async fn readdir_deep() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join("keep"); - - { - // read_dir should succeed. Collect all elements - let elements: Vec<_> = - ReadDirStream::new(tokio::fs::read_dir(p).await.expect("must succeed")) - .map(|e| e.expect("must not be err")) - .collect() - .await; - - assert_eq!(1, elements.len(), "number of elements should be 1"); // rust skips . and .. - - // ".keep", 0 byte file. - let e = &elements[0]; - assert_eq!(".keep", e.file_name()); - assert!(e.file_type().await.expect("must succeed").is_file()); - assert_eq!(0, e.metadata().await.expect("must succeed").len()); - } - - fuse_daemon.unmount().expect("unmount"); -} - -/// Check attributes match how they show up in /nix/store normally. -#[tokio::test] -async fn check_attributes() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_blob_a(&blob_service, &mut root_nodes).await; - populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; - populate_symlink(&mut root_nodes).await; - populate_blob_helloworld(&blob_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p_file = tmpdir.path().join(BLOB_A_NAME); - let p_directory = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); - let p_symlink = tmpdir.path().join(SYMLINK_NAME); - let p_executable_file = tmpdir.path().join(HELLOWORLD_BLOB_NAME); - - // peek at metadata. We use symlink_metadata to ensure we don't traverse a symlink by accident. - let metadata_file = tokio::fs::symlink_metadata(&p_file) - .await - .expect("must succeed"); - let metadata_executable_file = tokio::fs::symlink_metadata(&p_executable_file) - .await - .expect("must succeed"); - let metadata_directory = tokio::fs::symlink_metadata(&p_directory) - .await - .expect("must succeed"); - let metadata_symlink = tokio::fs::symlink_metadata(&p_symlink) - .await - .expect("must succeed"); - - // modes should match. We & with 0o777 to remove any higher bits. - assert_eq!(0o444, metadata_file.mode() & 0o777); - assert_eq!(0o555, metadata_executable_file.mode() & 0o777); - assert_eq!(0o555, metadata_directory.mode() & 0o777); - assert_eq!(0o444, metadata_symlink.mode() & 0o777); - - // files should have the correct filesize - assert_eq!(fixtures::BLOB_A.len() as u64, metadata_file.len()); - // directories should have their "size" as filesize - assert_eq!( - { fixtures::DIRECTORY_WITH_KEEP.size() }, - metadata_directory.size() - ); - - for metadata in &[&metadata_file, &metadata_directory, &metadata_symlink] { - // uid and gid should be 0. - assert_eq!(0, metadata.uid()); - assert_eq!(0, metadata.gid()); - - // all times should be set to the unix epoch. - assert_eq!(0, metadata.atime()); - assert_eq!(0, metadata.mtime()); - assert_eq!(0, metadata.ctime()); - // crtime seems MacOS only - } - - fuse_daemon.unmount().expect("unmount"); -} - -#[tokio::test] -/// Ensure we allocate the same inodes for the same directory contents. -/// $DIRECTORY_COMPLICATED_NAME/keep contains the same data as $DIRECTORY_WITH_KEEP. -async fn compare_inodes_directories() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_with_keep(&blob_service, &directory_service, &mut root_nodes).await; - populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p_dir_with_keep = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); - let p_sibling_dir = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join("keep"); - - // peek at metadata. - assert_eq!( - tokio::fs::metadata(p_dir_with_keep) - .await - .expect("must succeed") - .ino(), - tokio::fs::metadata(p_sibling_dir) - .await - .expect("must succeed") - .ino() - ); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Ensure we allocate the same inodes for the same directory contents. -/// $DIRECTORY_COMPLICATED_NAME/keep/,keep contains the same data as $DIRECTORY_COMPLICATED_NAME/.keep -#[tokio::test] -async fn compare_inodes_files() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p_keep1 = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join(".keep"); - let p_keep2 = tmpdir - .path() - .join(DIRECTORY_COMPLICATED_NAME) - .join("keep") - .join(".keep"); - - // peek at metadata. - assert_eq!( - tokio::fs::metadata(p_keep1) - .await - .expect("must succeed") - .ino(), - tokio::fs::metadata(p_keep2) - .await - .expect("must succeed") - .ino() - ); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Ensure we allocate the same inode for symlinks pointing to the same targets. -/// $DIRECTORY_COMPLICATED_NAME/aa points to the same target as SYMLINK_NAME2. -#[tokio::test] -async fn compare_inodes_symlinks() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directory_complicated(&blob_service, &directory_service, &mut root_nodes).await; - populate_symlink2(&mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p1 = tmpdir.path().join(DIRECTORY_COMPLICATED_NAME).join("aa"); - let p2 = tmpdir.path().join(SYMLINK_NAME2); - - // peek at metadata. - assert_eq!( - tokio::fs::symlink_metadata(p1) - .await - .expect("must succeed") - .ino(), - tokio::fs::symlink_metadata(p2) - .await - .expect("must succeed") - .ino() - ); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Check we match paths exactly. -#[tokio::test] -async fn read_wrong_paths_in_root() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_blob_a(&blob_service, &mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - // wrong name - assert!( - tokio::fs::metadata(tmpdir.path().join("00000000000000000000000000000000-tes")) - .await - .is_err() - ); - - // invalid hash - assert!( - tokio::fs::metadata(tmpdir.path().join("0000000000000000000000000000000-test")) - .await - .is_err() - ); - - // right name, must exist - assert!( - tokio::fs::metadata(tmpdir.path().join("00000000000000000000000000000000-test")) - .await - .is_ok() - ); - - // now wrong name with right hash still may not exist - assert!( - tokio::fs::metadata(tmpdir.path().join("00000000000000000000000000000000-tes")) - .await - .is_err() - ); - - fuse_daemon.unmount().expect("unmount"); -} - -/// Make sure writes are not allowed -#[tokio::test] -async fn disallow_writes() { - // https://plume.benboeckel.net/~/JustAnotherBlog/skipping-tests-in-rust - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let root_nodes = BTreeMap::default(); - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(BLOB_A_NAME); - let e = tokio::fs::File::create(p).await.expect_err("must fail"); - - assert_eq!(Some(libc::EROFS), e.raw_os_error()); - - fuse_daemon.unmount().expect("unmount"); -} - -#[tokio::test] -/// Ensure we get an IO error if the directory service does not have the Directory object. -async fn missing_directory() { - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_directorynode_without_directory(&mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(DIRECTORY_WITH_KEEP_NAME); - - { - // `stat` on the path should succeed, because it doesn't trigger the directory request. - tokio::fs::metadata(&p).await.expect("must succeed"); - - // However, calling either `readdir` or `stat` on a child should fail with an IO error. - // It fails when trying to pull the first entry, because we don't implement opendir separately - ReadDirStream::new(tokio::fs::read_dir(&p).await.unwrap()) - .next() - .await - .expect("must be some") - .expect_err("must be err"); - - // rust currently sets e.kind() to Uncategorized, which isn't very - // helpful, so we don't look at the error more closely than that.. - tokio::fs::metadata(p.join(".keep")) - .await - .expect_err("must fail"); - } - - fuse_daemon.unmount().expect("unmount"); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -/// Ensure we get an IO error if the blob service does not have the blob -async fn missing_blob() { - if !std::path::Path::new("/dev/fuse").exists() { - eprintln!("skipping test"); - return; - } - let tmpdir = TempDir::new().unwrap(); - - let (blob_service, directory_service) = gen_svcs(); - let mut root_nodes = BTreeMap::default(); - - populate_filenode_without_blob(&mut root_nodes).await; - - let mut fuse_daemon = do_mount( - blob_service, - directory_service, - root_nodes, - tmpdir.path(), - false, - false, - ) - .expect("must succeed"); - - let p = tmpdir.path().join(BLOB_A_NAME); - - { - // `stat` on the blob should succeed, because it doesn't trigger a request to the blob service. - tokio::fs::metadata(&p).await.expect("must succeed"); - - // However, calling read on the blob should fail. - // rust currently sets e.kind() to Uncategorized, which isn't very - // helpful, so we don't look at the error more closely than that.. - tokio::fs::read(p).await.expect_err("must fail"); - } - - fuse_daemon.unmount().expect("unmount"); -} -- cgit 1.4.1