mod common;
use common::*;
use hyperfile::file::hyper::Hyper;
use hyperfile::file::flags::FileFlags;
use hyperfile::file::mode::FileMode;
#[tokio::test]
#[ignore]
async fn smoke_write_read_round_trip() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload: Vec<u8> = (0u8..=127u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create failed");
let n = hyper.fs_write(0, &payload).await.expect("write failed");
assert_eq!(n, payload.len());
let _last_cno = hyper.fs_release().await.expect("release failed");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("open failed");
let stat = hyper.fs_getattr().expect("getattr failed");
assert_eq!(stat.st_size as usize, payload.len());
let mut buf = vec![0u8; payload.len()];
let n = hyper.fs_read(0, &mut buf).await.expect("read failed");
assert_eq!(n, payload.len());
assert_eq!(buf, payload);
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_extend() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload: Vec<u8> = (0..1024u16).map(|v| (v & 0xFF) as u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create failed");
let n = hyper.fs_write(0, &payload).await.expect("write failed");
assert_eq!(n, payload.len());
let _ = hyper.fs_release().await.expect("release failed");
}
let new_size = 8 * 1024;
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await
.expect("open failed");
hyper.fs_truncate(new_size).await.expect("truncate extend failed");
let _ = hyper.fs_release().await.expect("release failed");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("open failed");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, new_size);
let mut buf = vec![0u8; new_size];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, new_size);
assert_eq!(&buf[..payload.len()], &payload[..]);
assert!(buf[payload.len()..].iter().all(|&b| b == 0));
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload: Vec<u8> = (0..16 * 1024).map(|i| (i & 0xFF) as u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create failed");
let n = hyper.fs_write(0, &payload).await.expect("write failed");
assert_eq!(n, payload.len());
let _ = hyper.fs_release().await.expect("release");
}
let new_size = 5000;
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await
.expect("open failed");
hyper.fs_truncate(new_size).await.expect("truncate shrink failed");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("open failed");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, new_size);
let mut buf = vec![0u8; new_size];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, new_size);
assert_eq!(&buf[..], &payload[..new_size]);
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_trunc_truncates_to_zero_on_open() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload: Vec<u8> = (0..4096u16).map(|v| (v & 0xFF) as u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create failed");
let n = hyper.fs_write(0, &payload).await.expect("write failed");
assert_eq!(n, payload.len());
let _ = hyper.fs_release().await.expect("release");
}
{
let flags = FileFlags::from(libc::O_RDWR | libc::O_TRUNC);
let mut hyper = Hyper::fs_open(&client, tf.uri(), flags)
.await
.expect("open w/ O_TRUNC failed");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size, 0, "O_TRUNC should have set size to 0");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("open failed");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size, 0);
let mut buf = vec![0u8; 16];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, 0);
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_trunc_ignored_when_readonly() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload: Vec<u8> = (0..256u16).map(|v| (v & 0xFF) as u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
{
let flags = FileFlags::from(libc::O_RDONLY | libc::O_TRUNC);
let mut hyper = Hyper::fs_open(&client, tf.uri(), flags)
.await
.expect("open ro+trunc");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, payload.len(), "O_TRUNC must be a no-op without write access");
let mut buf = vec![0u8; payload.len()];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, payload.len());
assert_eq!(buf, payload);
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_noatime_skips_atime_update_on_read() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload = vec![0xABu8; 4096];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("open");
let before = hyper.fs_getattr().expect("getattr").st_atime;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let mut buf = vec![0u8; payload.len()];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
let after = hyper.fs_getattr().expect("getattr").st_atime;
assert!(
after > before,
"control: O_RDONLY read should advance atime (before={}, after={})",
before, after
);
let _ = hyper.fs_release().await;
}
{
let flags = FileFlags::from(libc::O_RDONLY | libc::O_NOATIME);
let mut hyper = Hyper::fs_open(&client, tf.uri(), flags)
.await
.expect("open w/ O_NOATIME");
let before = hyper.fs_getattr().expect("getattr").st_atime;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let mut buf = vec![0u8; payload.len()];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
let after = hyper.fs_getattr().expect("getattr").st_atime;
assert_eq!(
before, after,
"O_NOATIME read must not advance atime (before={}, after={})",
before, after
);
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_append_writes_at_end() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create");
let _ = hyper.fs_release().await.expect("release");
}
let part_a = b"hello-".to_vec();
let part_b = b"world\n".to_vec();
{
let flags = FileFlags::from(libc::O_RDWR | libc::O_APPEND);
let mut hyper = Hyper::fs_open(&client, tf.uri(), flags)
.await
.expect("open w/ O_APPEND");
let n = hyper.fs_write(999, &part_a).await.expect("write A");
assert_eq!(n, part_a.len());
let n = hyper.fs_write(0, &part_b).await.expect("write B");
assert_eq!(n, part_b.len());
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, part_a.len() + part_b.len());
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("reopen");
let total = part_a.len() + part_b.len();
let mut buf = vec![0u8; total];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, total);
let mut want = part_a.clone();
want.extend_from_slice(&part_b);
assert_eq!(buf, want);
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_append_write_zero() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let head = vec![0xCDu8; 100];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create");
let _ = hyper.fs_write(0, &head).await.expect("write head");
let _ = hyper.fs_release().await.expect("release");
}
{
let flags = FileFlags::from(libc::O_RDWR | libc::O_APPEND);
let mut hyper = Hyper::fs_open(&client, tf.uri(), flags)
.await
.expect("open");
let n = hyper.fs_write_zero(7777, 200).await.expect("write_zero");
assert_eq!(n, 200);
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, head.len() + 200);
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("reopen");
let total = head.len() + 200;
let mut buf = vec![0u8; total];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, total);
assert_eq!(&buf[..head.len()], &head[..]);
assert!(buf[head.len()..].iter().all(|&b| b == 0));
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_excl_creates_when_missing() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let flags = FileFlags::from(libc::O_RDWR | libc::O_CREAT | libc::O_EXCL);
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
flags,
FileMode::default_file(),
)
.await
.expect("O_CREAT|O_EXCL on missing file must succeed");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size, 0);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_excl_errors_when_exists() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("pre-create");
let _ = hyper.fs_release().await.expect("release");
}
let flags = FileFlags::from(libc::O_RDWR | libc::O_CREAT | libc::O_EXCL);
let res = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
flags,
FileMode::default_file(),
)
.await;
match res {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) => panic!("expected AlreadyExists, got {:?}", e),
Ok(_) => panic!("O_CREAT|O_EXCL on existing file must fail"),
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_creat_without_excl_opens_existing() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload = vec![0xEEu8; 256];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("pre-create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
let flags = FileFlags::from(libc::O_RDWR | libc::O_CREAT);
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
flags,
FileMode::default_file(),
)
.await
.expect("O_CREAT without O_EXCL on existing file must succeed");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, payload.len(), "existing content lost");
let mut buf = vec![0u8; payload.len()];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, payload.len());
assert_eq!(buf, payload);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_o_excl_without_creat_is_ignored_on_open() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("pre-create");
let _ = hyper.fs_release().await.expect("release");
}
let flags = FileFlags::from(libc::O_RDWR | libc::O_EXCL);
let mut hyper = Hyper::fs_open(&client, tf.uri(), flags)
.await
.expect("fs_open + O_EXCL must ignore the EXCL bit");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_sparse_st_blocks_does_not_inflate() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const ONE_GIB: usize = 1024 * 1024 * 1024;
const BLOCK: usize = 4096;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create");
let buf = vec![0xAA; BLOCK];
hyper.fs_write(0, &buf).await.expect("write head");
hyper.fs_write(ONE_GIB, &buf).await.expect("write tail");
let _ = hyper.fs_release().await.expect("release");
}
{
let hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("reopen");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(
stat.st_size as usize,
ONE_GIB + BLOCK,
"st_size should reflect the virtual file size"
);
let expected_blocks = (2 * BLOCK / 512) as i64;
let bug_value = ((ONE_GIB + BLOCK) / 512) as i64;
assert_eq!(
stat.st_blocks, expected_blocks,
"st_blocks should reflect actually-allocated 512-byte units \
(expected {}, got {}); the old buggy value would be {}",
expected_blocks, stat.st_blocks, bug_value
);
assert_eq!(
stat.st_blksize as usize, BLOCK,
"st_blksize should equal the data_block_size from meta config"
);
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink_decrements_st_blocks() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload: Vec<u8> = (0..BLOCK * 8).map(|i| (i & 0xFF) as u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client,
tf.uri(),
FileFlags::rdwr(),
FileMode::default_file(),
)
.await
.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await
.expect("reopen rdwr");
let stat_before = hyper.fs_getattr().expect("getattr before");
assert_eq!(stat_before.st_blocks, (8 * BLOCK / 512) as i64);
hyper.fs_truncate(BLOCK).await.expect("truncate to 1 block");
let _ = hyper.fs_release().await.expect("release");
}
{
let hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await
.expect("reopen ro");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, BLOCK);
assert_eq!(
stat.st_blocks,
(BLOCK / 512) as i64,
"after shrinking from 8 blocks to 1, st_blocks should be 8 (= 4096/512), not 64"
);
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink_to_block_boundary_preserves_data() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload = vec![0xAAu8; BLOCK * 2];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await.expect("reopen rdwr");
hyper.fs_truncate(BLOCK).await.expect("truncate to boundary");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, BLOCK);
let mut buf = vec![0u8; BLOCK];
let n = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(n, BLOCK);
assert_eq!(
buf,
payload[..BLOCK],
"truncate to a block boundary must keep the last fully-retained \
block intact; got the block partly or fully zeroed"
);
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_same_block_to_boundary() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload: Vec<u8> = (0..BLOCK + 1).map(|i| (i & 0xFF) as u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
hyper.fs_truncate(BLOCK).await.expect("truncate");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen");
let stat = hyper.fs_getattr().expect("getattr");
assert_eq!(stat.st_size as usize, BLOCK);
let mut buf = vec![0u8; BLOCK];
hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(buf, payload[..BLOCK], "block 0 content must be preserved");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_same_block_mid_to_mid() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload: Vec<u8> = (0..3000).map(|i| (i & 0xFF) as u8).collect();
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
hyper.fs_truncate(1500).await.expect("truncate");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen");
assert_eq!(hyper.fs_getattr().expect("ga").st_size as usize, 1500);
let mut buf = vec![0u8; 1500];
hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(buf, payload[..1500], "content [0,1500) must survive");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_cross_block_shrink_to_mid() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload = vec![0xAAu8; BLOCK * 3];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
hyper.fs_truncate(BLOCK + 904).await.expect("truncate to 5000");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, BLOCK + 904);
assert_eq!(stat.st_blocks, (2 * BLOCK / 512) as i64);
let mut buf = vec![0u8; BLOCK + 904];
hyper.fs_read(0, &mut buf).await.expect("read");
assert!(buf.iter().all(|&b| b == 0xAA), "content survives in [0, new_size)");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_extend_to_mid_block() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload = vec![0xCCu8; BLOCK];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
hyper.fs_truncate(BLOCK + 1234).await.expect("extend to 5330");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, BLOCK + 1234);
assert_eq!(stat.st_blocks, (BLOCK / 512) as i64);
let mut buf = vec![0u8; BLOCK + 1234];
hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(&buf[..BLOCK], &payload[..]);
assert!(buf[BLOCK..].iter().all(|&b| b == 0), "extended range reads as zeros");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_to_zero_then_write() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let initial = vec![0xAAu8; BLOCK * 2];
let final_payload = vec![0xBBu8; 1000];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &initial).await.expect("write 8 KiB");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await.expect("reopen");
hyper.fs_truncate(0).await.expect("truncate to zero");
let _ = hyper.fs_write(0, &final_payload).await.expect("write 1 KiB");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, final_payload.len());
assert_eq!(stat.st_blocks, (BLOCK / 512) as i64,
"should report 1 block (the new write), not 2 (stale from initial)");
let mut buf = vec![0u8; final_payload.len()];
hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(buf, final_payload);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_extend_then_shrink_back() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload = vec![0xCDu8; 100];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write 100");
hyper.fs_truncate(BLOCK * 4).await.expect("extend to 16 KiB");
hyper.fs_truncate(payload.len()).await.expect("shrink back to 100");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, payload.len());
let mut buf = vec![0u8; payload.len()];
hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(buf, payload);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink_to_one_byte() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload = vec![0xAAu8; BLOCK];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await.expect("reopen");
hyper.fs_truncate(1).await.expect("truncate to 1 byte");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size, 1);
let mut buf = vec![0u8; 1];
hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(buf[0], 0xAA);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink_through_zero_block() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &vec![0xAAu8; BLOCK]).await.expect("write block 0");
let _ = hyper.fs_write_zero(BLOCK, BLOCK).await.expect("write_zero block 1");
let _ = hyper.fs_write(BLOCK * 2, &vec![0xCCu8; BLOCK]).await.expect("write block 2");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await.expect("reopen rdwr");
hyper.fs_truncate(BLOCK + 904).await.expect("truncate to 5000");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, BLOCK + 904);
let mut buf = vec![0xFFu8; BLOCK + 904]; hyper.fs_read(0, &mut buf).await.expect("read");
assert!(buf[..BLOCK].iter().all(|&b| b == 0xAA), "block 0 must be 0xAA");
assert!(buf[BLOCK..].iter().all(|&b| b == 0), "tail of zero-block 1 must read as 0");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink_to_zero_block_boundary() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &vec![0xAAu8; BLOCK]).await.expect("write");
let _ = hyper.fs_write_zero(BLOCK, BLOCK).await.expect("write_zero");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await.expect("reopen rdwr");
hyper.fs_truncate(BLOCK).await.expect("truncate");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, BLOCK);
let mut buf = vec![0u8; BLOCK];
hyper.fs_read(0, &mut buf).await.expect("read");
assert!(buf.iter().all(|&b| b == 0xAA), "block 0 (0xAA) must survive");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink_with_unflushed_writes() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let payload = vec![0xAAu8; BLOCK * 3]; {
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
hyper.fs_truncate(BLOCK + 904).await
.expect("truncate-shrink with unflushed dirty cache must not fail");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, BLOCK + 904);
let mut buf = vec![0u8; BLOCK + 904];
hyper.fs_read(0, &mut buf).await.expect("read");
assert!(buf.iter().all(|&b| b == 0xAA),
"first 5000 bytes of 0xAA must survive truncate");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_fdatasync_skips_attr_only_flush() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload = vec![0xAAu8; 4096];
let baseline_cno = {
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
hyper.fs_flush().await.expect("flush baseline")
};
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("open ro");
let mut buf = vec![0u8; payload.len()];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
assert!(hyper.is_attr_dirty(),
"read should have dirtied atime in-memory");
assert_eq!(hyper.dirty_block_count(), 0);
let cno_after_fdatasync = hyper.fs_fdatasync().await
.expect("fdatasync attr-only");
assert_eq!(
cno_after_fdatasync, baseline_cno,
"fdatasync must not bump cno"
);
assert!(hyper.is_attr_dirty(),
"fdatasync must leave attr-dirty bit SET (skip = no inode write)");
let _ = hyper.fs_release().await;
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("open ro");
let mut buf = vec![0u8; payload.len()];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
assert!(hyper.is_attr_dirty());
let _ = hyper.fs_flush().await.expect("flush attr-only");
assert!(!hyper.is_attr_dirty(),
"fs_flush must clear attr-dirty bit (inode written to S3)");
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_fdatasync_persists_data() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let baseline_cno = {
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
hyper.fs_release().await.expect("release")
};
let payload = vec![0xBBu8; 4096];
let after_fdatasync_cno = {
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdwr())
.await.expect("open rdwr");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let cno = hyper.fs_fdatasync().await.expect("fdatasync data-dirty");
let _ = hyper.fs_release().await;
cno
};
assert!(
after_fdatasync_cno > baseline_cno,
"fdatasync with dirty data must bump cno (got {} <= {})",
after_fdatasync_cno, baseline_cno
);
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, payload.len());
let mut buf = vec![0u8; payload.len()];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(buf, payload);
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_ctime_bumps_on_metadata_changes() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let tick = || async {
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
};
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let ctime0 = hyper.fs_getattr().expect("ga0").st_ctime;
tick().await;
let _ = hyper.fs_write(0, &vec![0xAAu8; 100]).await.expect("write");
let st1 = hyper.fs_getattr().expect("ga1");
assert!(st1.st_ctime > ctime0,
"write must bump ctime ({} > {})", st1.st_ctime, ctime0);
assert_eq!(st1.st_ctime, st1.st_mtime,
"write should bump mtime and ctime to the same instant");
tick().await;
let mut buf = vec![0u8; 100];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
let st2 = hyper.fs_getattr().expect("ga2");
assert_eq!(st2.st_ctime, st1.st_ctime,
"read must NOT bump ctime (got {} != {})", st2.st_ctime, st1.st_ctime);
tick().await;
hyper.fs_truncate(50).await.expect("truncate");
let st3 = hyper.fs_getattr().expect("ga3");
assert!(st3.st_ctime > st2.st_ctime,
"truncate must bump ctime ({} > {})", st3.st_ctime, st2.st_ctime);
tick().await;
let _ = hyper.fs_chmod(0o600).await.expect("chmod");
let st4 = hyper.fs_getattr().expect("ga4");
assert!(st4.st_ctime > st3.st_ctime,
"chmod must bump ctime ({} > {})", st4.st_ctime, st3.st_ctime);
assert_eq!(st4.st_mtime, st3.st_mtime,
"chmod must not bump mtime (got {} != {})", st4.st_mtime, st3.st_mtime);
tick().await;
let _ = hyper.fs_chown(1234, 5678).await.expect("chown");
let st5 = hyper.fs_getattr().expect("ga5");
assert!(st5.st_ctime > st4.st_ctime,
"chown must bump ctime ({} > {})", st5.st_ctime, st4.st_ctime);
assert_eq!(st5.st_mtime, st4.st_mtime,
"chown must not bump mtime");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_setattr_overrides_caller_ctime() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let ctime0 = hyper.fs_getattr().expect("ga0").st_ctime;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let mut stat = hyper.fs_getattr().expect("ga1");
stat.st_ctime = 0;
stat.st_ctime_nsec = 0;
stat.st_uid = 4242;
let returned = hyper.fs_setattr(&stat).await.expect("setattr");
assert!(returned.st_ctime > ctime0,
"setattr must bump ctime to NOW, ignoring caller's value (got {})",
returned.st_ctime);
assert_ne!(returned.st_ctime, 0);
assert_eq!(returned.st_uid, 4242);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_fs_rename_returns_unsupported() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let payload = vec![0xCDu8; 128];
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &payload).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
let dst = format!("{}-renamed-target", tf.uri());
let res = Hyper::fs_rename(&client, tf.uri(), &dst).await;
match res {
Err(e) if e.kind() == std::io::ErrorKind::Unsupported => {}
Err(e) => panic!("expected Unsupported, got {:?}", e),
Ok(()) => panic!("fs_rename returned Ok but is not yet implemented"),
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("source still openable");
let stat = hyper.fs_getattr().expect("ga");
assert_eq!(stat.st_size as usize, payload.len());
let mut buf = vec![0u8; payload.len()];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
assert_eq!(buf, payload);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_seek_data_hole_flushed() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
hyper.fs_write(0, &vec![0xAAu8; BLOCK]).await.expect("write block 0");
hyper.fs_write(BLOCK * 2, &vec![0xCCu8; BLOCK]).await.expect("write block 2");
hyper.fs_truncate(BLOCK * 4).await.expect("truncate to 16 KiB");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
assert_eq!(hyper.fs_seek_data(0).await.expect("sd0"), Some(0));
assert_eq!(hyper.fs_seek_hole(0).await.expect("sh0"), Some(BLOCK));
assert_eq!(hyper.fs_seek_data(BLOCK).await.expect("sd1"), Some(BLOCK * 2));
assert_eq!(hyper.fs_seek_data(100).await.expect("sd100"), Some(100));
assert_eq!(hyper.fs_seek_hole(BLOCK * 2 + 10).await.expect("sh2"), Some(BLOCK * 3));
assert_eq!(hyper.fs_seek_data(BLOCK * 3).await.expect("sd3"), None);
assert_eq!(hyper.fs_seek_hole(BLOCK * 4).await.expect("shEOF"), Some(BLOCK * 4));
assert_eq!(hyper.fs_seek_data(BLOCK * 4).await.expect("sdEOF"), None);
assert_eq!(hyper.fs_seek_hole(BLOCK * 5).await.expect("sh>"), None);
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_seek_data_hole_unflushed() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
hyper.fs_write(BLOCK, &vec![0xBBu8; BLOCK]).await.expect("write block 1");
if hyper.fs_getattr().expect("ga").st_size < (BLOCK * 3) as i64 {
hyper.fs_truncate(BLOCK * 3).await.expect("extend");
}
assert_eq!(hyper.fs_seek_data(0).await.expect("sd0"), Some(BLOCK));
assert_eq!(hyper.fs_seek_hole(0).await.expect("sh0"), Some(0));
assert_eq!(hyper.fs_seek_hole(BLOCK).await.expect("sh1"), Some(BLOCK * 2));
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_create_stamps_all_times() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let st = hyper.fs_getattr().expect("getattr");
assert!(st.st_atime > 0, "atime must not be epoch 0");
assert!(st.st_mtime > 0, "mtime must not be epoch 0");
assert!(st.st_ctime > 0, "ctime must not be epoch 0");
for (name, t) in [("atime", st.st_atime), ("mtime", st.st_mtime), ("ctime", st.st_ctime)] {
assert!((t - now).abs() < 120, "{} ({}) should be ≈ now ({})", name, t, now);
}
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_shrink_drops_clean_cached_blocks() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const OFF: usize = 200_000;
const LEN: usize = 4096;
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let n = hyper.fs_write(OFF, &vec![0xABu8; LEN]).await.expect("write");
assert_eq!(n, LEN);
let _ = hyper.fs_flush().await.expect("flush");
hyper.fs_truncate(1000).await.expect("shrink to 1000");
let st = hyper.fs_getattr().expect("ga after shrink");
assert_eq!(st.st_size as usize, 1000);
hyper.fs_truncate(300_000).await.expect("grow to 300000");
let st = hyper.fs_getattr().expect("ga after grow");
assert_eq!(st.st_size as usize, 300_000);
let mut buf = vec![0xFFu8; LEN]; let n = hyper.fs_read(OFF, &mut buf).await.expect("read");
assert_eq!(n, LEN);
let nonzero = buf.iter().filter(|&&b| b != 0).count();
assert_eq!(nonzero, 0,
"grown-back region must read as a hole, found {} stale bytes (first={:#x})",
nonzero, buf.iter().find(|&&b| b != 0).copied().unwrap_or(0));
let _ = hyper.fs_release().await.expect("release");
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let mut buf = vec![0xFFu8; LEN];
let _ = hyper.fs_read(OFF, &mut buf).await.expect("read after reopen");
assert!(buf.iter().all(|&b| b == 0), "reopened handle must also read zeros");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_truncate_same_block_tail_persists_after_bmap_spill() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLK: usize = 4096;
const FULL: usize = 30;
const TAIL: usize = 500;
const KEEP: usize = 100;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let size = FULL * BLK + TAIL;
let n = hyper.fs_write(0, &vec![0xABu8; size]).await.expect("write");
assert_eq!(n, size);
let _ = hyper.fs_flush().await.expect("flush");
hyper.fs_truncate(FULL * BLK + KEEP).await.expect("shrink within the last block");
let _ = hyper.fs_flush().await.expect("flush after shrink");
hyper.fs_truncate(FULL * BLK + TAIL).await.expect("grow back");
let _ = hyper.fs_flush().await.expect("flush after grow");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen ro");
let mut buf = vec![0xFFu8; TAIL];
let n = hyper.fs_read(FULL * BLK, &mut buf).await.expect("read the last block");
assert_eq!(n, TAIL);
assert!(buf[0..KEEP].iter().all(|&b| b == 0xAB),
"the retained head of the block must survive the shrink");
let stale = buf[KEEP..].iter().filter(|&&b| b != 0).count();
assert_eq!(stale, 0,
"the region discarded by the shrink must read as zeros, found {} stale bytes (first={:#04x})",
stale, buf[KEEP..].iter().find(|&&b| b != 0).copied().unwrap_or(0));
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_rdonly_handle_write_ops_are_ebadf() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &vec![0xAAu8; 4096]).await.expect("write rdwr");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen rdonly");
let expect_ebadf = |res: std::io::Result<usize>, what: &str| {
let err = res.err().unwrap_or_else(|| panic!("{what} on a read-only handle must fail"));
assert_eq!(err.raw_os_error(), Some(libc::EBADF),
"{what} must fail with EBADF, got {:?} (kind {:?})", err.raw_os_error(), err.kind());
};
expect_ebadf(hyper.fs_write(0, &[0xBBu8; 8]).await, "fs_write");
expect_ebadf(hyper.fs_write_zero(0, 8).await, "fs_write_zero");
expect_ebadf(hyper.fs_truncate(0).await.map(|_| 0usize), "fs_truncate");
expect_ebadf(hyper.fs_truncate(1 << 20).await.map(|_| 0usize), "fs_truncate (extend)");
let mut buf = vec![0u8; 4096];
let n = hyper.fs_read(0, &mut buf).await.expect("read on rdonly handle");
assert_eq!(n, 4096);
assert!(buf.iter().all(|&b| b == 0xAA), "content must be untouched");
let st = hyper.fs_getattr().expect("getattr");
assert_eq!(st.st_size, 4096, "size must be untouched by the rejected truncates");
let _ = hyper.fs_flush().await.expect("fs_flush on rdonly handle");
let _ = hyper.fs_fdatasync().await.expect("fs_fdatasync on rdonly handle");
let st = hyper.fs_chmod(0o600).await.expect("fs_chmod on rdonly handle");
assert_eq!(st.st_mode & 0o777, 0o600, "chmod must take effect");
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_wronly_handle_read_is_ebadf() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &vec![0xAAu8; 4096]).await.expect("seed write");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::wronly())
.await.expect("reopen wronly");
let mut buf = vec![0u8; 4096];
let err = hyper.fs_read(0, &mut buf).await
.expect_err("read on a write-only handle must fail");
assert_eq!(err.raw_os_error(), Some(libc::EBADF),
"read must fail with EBADF, got {:?} (kind {:?})", err.raw_os_error(), err.kind());
let n = hyper.fs_write(0, &vec![0xBBu8; 4096]).await.expect("write on wronly handle");
assert_eq!(n, 4096);
let _ = hyper.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_wronly_handle_internal_reads_still_work() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &vec![0xAAu8; BLOCK * 2]).await.expect("seed write");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::wronly())
.await.expect("reopen wronly");
let n = hyper.fs_write(50, &vec![0xBBu8; 100]).await
.expect("partial write on wronly handle must work (read-modify-write)");
assert_eq!(n, 100);
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen rdonly");
let mut buf = vec![0u8; BLOCK * 2];
let _ = hyper.fs_read(0, &mut buf).await.expect("read");
assert!(buf[..50].iter().all(|&b| b == 0xAA), "bytes before the patch must be 0xAA");
assert!(buf[50..150].iter().all(|&b| b == 0xBB), "patched bytes must be 0xBB");
assert!(buf[150..].iter().all(|&b| b == 0xAA), "bytes after the patch must be 0xAA");
let _ = hyper.fs_release().await;
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::wronly())
.await.expect("reopen wronly for truncate");
hyper.fs_truncate(BLOCK + 1000).await
.expect("truncate on wronly handle must work");
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly())
.await.expect("reopen rdonly after truncate");
let st = hyper.fs_getattr().expect("getattr");
assert_eq!(st.st_size as usize, BLOCK + 1000);
let mut buf = vec![0u8; BLOCK + 1000];
let _ = hyper.fs_read(0, &mut buf).await.expect("read after truncate");
assert!(buf[50..150].iter().all(|&b| b == 0xBB), "patch must survive the truncate");
assert!(buf[BLOCK..].iter().all(|&b| b == 0xAA), "retained tail must be 0xAA");
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn smoke_wronly_handle_seek_data_hole_still_work() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &vec![0xAAu8; BLOCK]).await.expect("seed write");
hyper.fs_truncate(BLOCK * 3).await.expect("extend with a trailing hole");
let _ = hyper.fs_release().await.expect("release");
}
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::wronly())
.await.expect("reopen wronly");
assert_eq!(hyper.fs_seek_data(0).await.expect("seek_data on wronly"), Some(0));
assert_eq!(hyper.fs_seek_hole(0).await.expect("seek_hole on wronly"), Some(BLOCK));
let mut buf = vec![0u8; 16];
let err = hyper.fs_read(0, &mut buf).await.expect_err("read must still fail");
assert_eq!(err.raw_os_error(), Some(libc::EBADF));
let _ = hyper.fs_release().await;
tf.cleanup(&client).await;
}
async fn list_keys_under(client: &aws_sdk_s3::Client, uri: &str) -> Vec<String> {
let rest = uri.strip_prefix("s3://").expect("s3:// uri");
let (bucket, prefix) = rest.split_once('/').expect("uri has a key part");
let mut out = Vec::new();
let mut stream = client
.list_objects_v2()
.bucket(bucket)
.prefix(format!("{}/", prefix))
.into_paginator()
.send();
while let Some(page) = stream.next().await {
let page = page.expect("list_objects_v2");
for obj in page.contents.unwrap_or_default() {
if let Some(k) = obj.key {
out.push(k);
}
}
}
out
}
#[tokio::test]
#[ignore]
async fn smoke_unlink_nonexistent_is_enoent() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let err = Hyper::fs_unlink(&client, tf.uri()).await
.expect_err("unlink of a non-existent file must fail");
assert_eq!(err.kind(), std::io::ErrorKind::NotFound,
"expected ENOENT, got {:?}", err.kind());
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), FileFlags::rdwr(), FileMode::default_file(),
).await.expect("create");
let _ = hyper.fs_write(0, &vec![0xAAu8; 4096]).await.expect("write");
let _ = hyper.fs_release().await.expect("release");
}
Hyper::fs_unlink(&client, tf.uri()).await.expect("unlink of an existing file");
assert!(list_keys_under(&client, tf.uri()).await.is_empty(),
"unlink must remove every object under the prefix");
let err = Hyper::fs_unlink(&client, tf.uri()).await
.expect_err("second unlink must fail");
assert_eq!(err.kind(), std::io::ErrorKind::NotFound,
"expected ENOENT on the second unlink, got {:?}", err.kind());
}
#[tokio::test]
#[ignore]
async fn smoke_o_direct_read_write_does_not_panic() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
const BLOCK: usize = 4096;
let direct = FileFlags::from(libc::O_RDWR | libc::O_DIRECT);
{
let mut hyper = Hyper::fs_open_or_create_with_default_opt(
&client, tf.uri(), direct, FileMode::default_file(),
).await.expect("create with O_DIRECT");
let n = hyper.fs_write(0, &vec![0xAAu8; BLOCK * 2]).await
.expect("write with O_DIRECT");
assert_eq!(n, BLOCK * 2);
let _ = hyper.fs_flush().await.expect("flush");
let mut buf = vec![0u8; BLOCK * 2];
let n = hyper.fs_read(0, &mut buf).await.expect("read with O_DIRECT");
assert_eq!(n, BLOCK * 2);
assert!(buf.iter().all(|&b| b == 0xAA), "content must round-trip");
let n = hyper.fs_write(50, &vec![0xBBu8; 100]).await
.expect("partial write with O_DIRECT");
assert_eq!(n, 100);
let _ = hyper.fs_release().await.expect("release");
}
{
let mut hyper = Hyper::fs_open(&client, tf.uri(), FileFlags::from(libc::O_RDONLY | libc::O_DIRECT))
.await.expect("reopen with O_DIRECT");
let mut buf = vec![0u8; BLOCK * 2];
let n = hyper.fs_read(0, &mut buf).await.expect("read after reopen");
assert_eq!(n, BLOCK * 2);
assert!(buf[..50].iter().all(|&b| b == 0xAA));
assert!(buf[50..150].iter().all(|&b| b == 0xBB));
assert!(buf[150..].iter().all(|&b| b == 0xAA));
let _ = hyper.fs_release().await;
}
tf.cleanup(&client).await;
}