#[allow(dead_code)]
mod common;
use common::*;
use hyperfile::file::hyper::Hyper;
use hyperfile::file::flags::FileFlags;
use hyperfile::file::mode::FileMode;
use hyperfile::file::block::BlockState;
const BLK: usize = 4096;
async fn open_rdwr(client: &aws_sdk_s3::Client, uri: &str) -> Hyper<'static> {
Hyper::fs_open_or_create_with_default_opt(
client, uri, FileFlags::rdwr(), FileMode::default_file(),
).await.expect("open rdwr")
}
#[tokio::test]
#[ignore]
async fn block_read_returns_written_data() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let mut h = open_rdwr(&client, tf.uri()).await;
let mut payload = vec![0u8; 3 * BLK];
payload[0..BLK].fill(0xA1);
payload[BLK..2 * BLK].fill(0xB2);
payload[2 * BLK..].fill(0xC3);
let _ = h.fs_write(0, &payload).await.expect("write");
for (idx, want) in [(0u64, 0xA1u8), (1, 0xB2), (2, 0xC3)] {
let blk = h.fs_block(idx).await.expect("fs_block").expect("block should be mapped");
assert_eq!(blk.len(), BLK, "borrow must be exactly one block long");
assert_eq!(blk.index(), idx);
assert!(blk.as_slice().iter().all(|b| *b == want),
"block {idx} should be all {want:#04x} while dirty");
}
assert_eq!(h.fs_block_state(1).await.expect("state"), BlockState::Mapped);
let _ = h.fs_flush().await.expect("flush");
for (idx, want) in [(0u64, 0xA1u8), (1, 0xB2), (2, 0xC3)] {
let blk = h.fs_block(idx).await.expect("fs_block").expect("block should be mapped");
assert!(blk.as_slice().iter().all(|b| *b == want),
"block {idx} should still be all {want:#04x} after flush");
}
for _ in 0..3 {
let blk = h.fs_block(1).await.expect("fs_block").expect("mapped");
assert_eq!(blk.as_slice()[0], 0xB2);
}
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn block_distinguishes_hole_zero_and_data() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0u8; BLK]).await.expect("write zeros as data");
let _ = h.fs_write(2 * BLK, &vec![0xDDu8; BLK]).await.expect("write block 2");
let _ = h.fs_write_zero(3 * BLK, BLK).await.expect("write_zero block 3");
let blk0 = h.fs_block(0).await.expect("fs_block 0");
assert!(blk0.is_some(), "a block of written zeros is data, not a hole");
assert!(blk0.unwrap().as_slice().iter().all(|b| *b == 0));
assert_eq!(h.fs_block_state(0).await.expect("state 0"), BlockState::Mapped);
assert!(h.fs_block(1).await.expect("fs_block 1").is_none(),
"a hole must not yield a borrow");
assert_eq!(h.fs_block_state(1).await.expect("state 1"), BlockState::Unmapped);
assert_eq!(h.fs_block_state(2).await.expect("state 2"), BlockState::Mapped);
assert_eq!(h.fs_block(2).await.expect("fs_block 2").expect("mapped").as_slice()[0], 0xDD);
assert!(h.fs_block(3).await.expect("fs_block 3").is_none(),
"an explicit zero block has no data to borrow");
assert_eq!(h.fs_block_state(3).await.expect("state 3"), BlockState::Zero);
let mut buf = vec![0xFFu8; BLK];
let _ = h.fs_read(0, &mut buf).await.expect("read 0");
assert!(buf.iter().all(|b| *b == 0));
let mut buf = vec![0xFFu8; BLK];
let _ = h.fs_read(BLK, &mut buf).await.expect("read 1");
assert!(buf.iter().all(|b| *b == 0), "hole reads as zeros");
let mut buf = vec![0xFFu8; BLK];
let _ = h.fs_read(3 * BLK, &mut buf).await.expect("read 3");
assert!(buf.iter().all(|b| *b == 0), "zero block reads as zeros");
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn block_mut_modifies_in_place_and_persists() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0x11u8; 2 * BLK]).await.expect("write");
let _ = h.fs_flush().await.expect("flush");
let _ = h.fs_release().await.expect("release");
}
{
let mut h = open_rdwr(&client, tf.uri()).await;
{
let mut blk = h.fs_block_mut(1, false).await.expect("fs_block_mut")
.expect("block 1 is mapped");
assert_eq!(blk.len(), BLK);
assert!(blk.as_slice().iter().all(|b| *b == 0x11),
"block_mut must load existing content, not hand back a blank buffer");
blk.as_mut_slice()[0..4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
blk.as_mut_slice()[BLK - 1] = 0x99;
}
let _ = h.fs_flush().await.expect("flush");
let _ = h.fs_release().await.expect("release");
}
{
let mut h = open_rdwr(&client, tf.uri()).await;
let mut buf = vec![0u8; BLK];
let _ = h.fs_read(BLK, &mut buf).await.expect("read block 1");
assert_eq!(&buf[0..4], &[0xDE, 0xAD, 0xBE, 0xEF],
"in-place modification was not persisted");
assert_eq!(buf[BLK - 1], 0x99);
assert!(buf[4..BLK - 1].iter().all(|b| *b == 0x11),
"the rest of the block must be untouched");
let mut buf0 = vec![0u8; BLK];
let _ = h.fs_read(0, &mut buf0).await.expect("read block 0");
assert!(buf0.iter().all(|b| *b == 0x11));
let _ = h.fs_release().await.expect("release");
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn block_mut_create_semantics() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0x77u8; BLK]).await.expect("write");
let _ = h.fs_flush().await.expect("flush");
assert!(h.fs_block_mut(5, false).await.expect("block_mut no-create").is_none(),
"create=false must not materialize a hole");
assert_eq!(h.fs_block_state(5).await.expect("state"), BlockState::Unmapped,
"a rejected create must not have touched the bmap");
{
let mut blk = h.fs_block_mut(5, true).await.expect("block_mut create")
.expect("create=true must yield a block");
assert!(blk.as_slice().iter().all(|b| *b == 0),
"a created block must be zero-filled");
blk.as_mut_slice()[0..3].copy_from_slice(&[1, 2, 3]);
}
assert_eq!(h.fs_block_state(5).await.expect("state after create"), BlockState::Mapped);
let _ = h.fs_flush().await.expect("flush");
let _ = h.fs_release().await.expect("release");
let mut h = open_rdwr(&client, tf.uri()).await;
let blk = h.fs_block(5).await.expect("fs_block").expect("created block is mapped");
assert_eq!(&blk.as_slice()[0..3], &[1, 2, 3]);
assert!(blk.as_slice()[3..].iter().all(|b| *b == 0),
"the untouched remainder of a created block must be zeros");
drop(blk);
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn block_mut_repeated_borrows_make_one_version() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0u8; BLK]).await.expect("write");
let _ = h.fs_flush().await.expect("flush");
let before = h.fs_last_cno();
for i in 0..10u8 {
let mut blk = h.fs_block_mut(0, false).await.expect("block_mut").expect("mapped");
blk.as_mut_slice()[i as usize] = i + 1;
}
let _ = h.fs_flush().await.expect("flush");
let after = h.fs_last_cno();
assert_eq!(after, before + 1,
"ten borrows in one flush window produced {} checkpoints, expected 1",
after - before);
let blk = h.fs_block(0).await.expect("fs_block").expect("mapped");
for i in 0..10u8 {
assert_eq!(blk.as_slice()[i as usize], i + 1, "byte {i} lost");
}
drop(blk);
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn block_mut_leaves_i_size_alone() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0x55u8; BLK]).await.expect("write");
let _ = h.fs_flush().await.expect("flush");
let size_before = h.fs_getattr().expect("getattr").st_size;
let blocks_before = h.fs_getattr().expect("getattr").st_blocks;
assert_eq!(size_before, BLK as i64);
{
let mut blk = h.fs_block_mut(100, true).await.expect("block_mut").expect("created");
blk.as_mut_slice().fill(0xEE);
}
let _ = h.fs_flush().await.expect("flush");
let attr = h.fs_getattr().expect("getattr");
assert_eq!(attr.st_size, size_before,
"fs_block_mut must not change i_size");
assert!(attr.st_blocks > blocks_before,
"i_blocks should account for the new block: {} -> {}", blocks_before, attr.st_blocks);
let mut buf = vec![0u8; BLK];
let n = h.fs_read(100 * BLK, &mut buf).await.expect("read past EOF");
assert_eq!(n, 0, "read past i_size must return 0");
let _ = h.fs_release().await.expect("release");
let mut h = open_rdwr(&client, tf.uri()).await;
let blk = h.fs_block(100).await.expect("fs_block").expect("block above EOF is durable");
assert!(blk.as_slice().iter().all(|b| *b == 0xEE));
drop(blk);
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn block_api_enforces_access_mode() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0x33u8; BLK]).await.expect("write");
let _ = h.fs_flush().await.expect("flush");
let _ = h.fs_release().await.expect("release");
}
{
let mut h = Hyper::fs_open(&client, tf.uri(), FileFlags::rdonly()).await.expect("open rdonly");
assert!(h.fs_block(0).await.expect("read borrow on rdonly").is_some());
let e = h.fs_block_mut(0, false).await.expect_err("write borrow on rdonly must fail");
assert_eq!(e.raw_os_error(), Some(libc::EBADF), "expected EBADF, got {e}");
let _ = h.fs_release().await.expect("release");
}
{
let mut h = Hyper::fs_open(&client, tf.uri(), FileFlags::wronly()).await.expect("open wronly");
let e = h.fs_block(0).await.expect_err("read borrow on wronly must fail");
assert_eq!(e.raw_os_error(), Some(libc::EBADF), "expected EBADF, got {e}");
let e = h.fs_block_state(0).await.expect_err("state query needs read access");
assert_eq!(e.raw_os_error(), Some(libc::EBADF), "expected EBADF, got {e}");
assert!(h.fs_block_mut(0, false).await.expect("write borrow on wronly").is_some());
let _ = h.fs_flush().await.expect("flush");
let _ = h.fs_release().await.expect("release");
}
tf.cleanup(&client).await;
}
use hyperfile::config::{HyperFileConfigBuilder, HyperFileRuntimeConfig};
use hyperfile::data_cache::config::HyperFileDataCacheConfig;
use hyperfile::staging::config::StagingConfig;
async fn round_trip_with_cache(
cache: HyperFileDataCacheConfig,
data_cache_blocks: usize,
create_idx: u64,
label: &str,
) {
let client = make_client().await;
let tf = TestFile::new(&client).await;
{
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0x11u8; 4 * BLK]).await.expect("seed write low");
let _ = h.fs_write(12 * BLK, &vec![0x22u8; 4 * BLK]).await.expect("seed write high");
let _ = h.fs_flush().await.expect("seed flush");
assert_eq!(h.fs_getattr().expect("getattr").st_size, 16 * BLK as i64);
let _ = h.fs_release().await.expect("seed release");
}
let mut runtime = HyperFileRuntimeConfig::default();
runtime.data_cache_blocks = data_cache_blocks;
let staging = StagingConfig::new_s3_uri(tf.uri(), None);
let config = HyperFileConfigBuilder::new()
.with_staging_config(&staging)
.with_data_cache_config(&cache)
.with_runtime_config(&runtime)
.build();
{
let mut h = Hyper::fs_open_or_create_with_config(
&client, config.clone(), FileFlags::rdwr(), FileMode::default_file(),
).await.unwrap_or_else(|e| panic!("{label}: open: {e}"));
for _ in 0..3 {
for idx in 0..4u64 {
let blk = h.fs_block(idx).await
.unwrap_or_else(|e| panic!("{label}: fs_block({idx}): {e}"))
.unwrap_or_else(|| panic!("{label}: block {idx} should be mapped"));
assert!(blk.as_slice().iter().all(|b| *b == 0x11),
"{label}: block {idx} content wrong");
}
}
assert!(h.fs_block(6).await.expect("fs_block hole").is_none(),
"{label}: block 6 should be a hole");
{
let mut blk = h.fs_block_mut(2, false).await
.unwrap_or_else(|e| panic!("{label}: fs_block_mut: {e}"))
.unwrap_or_else(|| panic!("{label}: block 2 should be mapped"));
assert!(blk.as_slice().iter().all(|b| *b == 0x11),
"{label}: block_mut loaded wrong content");
blk.as_mut_slice()[0..2].copy_from_slice(&[0xAB, 0xCD]);
}
{
let mut blk = h.fs_block_mut(create_idx, true).await
.unwrap_or_else(|e| panic!("{label}: fs_block_mut create: {e}"))
.unwrap_or_else(|| panic!("{label}: create=true must yield a block"));
assert!(blk.as_slice().iter().all(|b| *b == 0),
"{label}: created block is not zero-filled");
blk.as_mut_slice()[0] = 0x5A;
}
let _ = h.fs_flush().await.expect("flush");
let _ = h.fs_release().await.expect("release");
}
{
let mut h = open_rdwr(&client, tf.uri()).await;
let mut buf = vec![0u8; BLK];
let _ = h.fs_read(2 * BLK, &mut buf).await.expect("read");
assert_eq!(&buf[0..2], &[0xAB, 0xCD], "{label}: modification not persisted");
assert!(buf[2..].iter().all(|b| *b == 0x11), "{label}: block 2 remainder damaged");
let blk = h.fs_block(create_idx).await.expect("fs_block created").expect("created block persisted");
assert_eq!(blk.as_slice()[0], 0x5A, "{label}: created block byte lost");
assert!(blk.as_slice()[1..].iter().all(|b| *b == 0),
"{label}: created block remainder is not zeros");
drop(blk);
let _ = h.fs_release().await.expect("release");
}
tf.cleanup(&client).await;
}
#[tokio::test]
#[ignore]
async fn block_api_on_local_disk_cache() {
let _ = env_logger::try_init();
let dir = format!("/tmp/hyperfile-block-api-test-{}", std::process::id());
std::fs::create_dir_all(&dir).expect("create cache dir");
round_trip_with_cache(
HyperFileDataCacheConfig::new_local_disk(Some(&dir), None),
64,
40,
"local-disk cache",
).await;
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
#[ignore]
async fn block_api_with_data_cache_disabled() {
let _ = env_logger::try_init();
round_trip_with_cache(HyperFileDataCacheConfig::new_mem(), 0, 20, "disabled cache").await;
}
#[tokio::test]
#[ignore]
async fn block_mut_edit_survives_after_the_bmap_spills() {
let _ = env_logger::try_init();
let client = make_client().await;
for (first, count) in [(0u64, 8u64), (0, 30), (33554432, 8)] {
let tf = TestFile::new(&client).await;
{
let mut h = open_rdwr(&client, tf.uri()).await;
for i in first..first + count {
let mut blk = h.fs_block_mut(i, true).await
.unwrap_or_else(|e| panic!("create block {i}: {e}"))
.expect("create=true yields a block");
blk.as_mut_slice().fill(0xAA);
}
let _ = h.fs_flush().await.expect("flush 1");
for i in first..first + count {
let mut blk = h.fs_block_mut(i, false).await
.unwrap_or_else(|e| panic!("edit block {i}: {e}"))
.unwrap_or_else(|| panic!("block {i} should be mapped"));
blk.as_mut_slice().fill(0xBB);
}
for i in first..first + count {
let blk = h.fs_block(i).await.expect("warm").expect("mapped");
assert_eq!(blk.as_slice()[0], 0xBB,
"first={first} count={count}: block {i} wrong even warm");
}
let _ = h.fs_flush().await.expect("flush 2");
let _ = h.fs_release().await.expect("release");
}
{
let mut h = open_rdwr(&client, tf.uri()).await;
let mut lost = Vec::new();
for i in first..first + count {
let blk = h.fs_block(i).await
.unwrap_or_else(|e| panic!("cold read block {i}: {e}"))
.unwrap_or_else(|| panic!("block {i} should be mapped"));
if blk.as_slice()[0] != 0xBB {
lost.push((i, blk.as_slice()[0]));
}
}
assert!(lost.is_empty(),
"first={first} count={count}: {} of {count} in-place edits lost; \
a cold reader still sees the previous version for {:?}",
lost.len(), lost.iter().take(5).collect::<Vec<_>>());
let _ = h.fs_release().await.expect("release");
}
tf.cleanup(&client).await;
}
}
#[tokio::test]
#[ignore]
async fn block_mut_and_byte_writes_interoperate_after_a_spill() {
let _ = env_logger::try_init();
let client = make_client().await;
const N: u64 = 12;
{
let tf = TestFile::new(&client).await;
{
let mut h = open_rdwr(&client, tf.uri()).await;
let _ = h.fs_write(0, &vec![0xAAu8; N as usize * BLK]).await.expect("write");
let _ = h.fs_flush().await.expect("flush 1");
for i in 0..N {
let mut blk = h.fs_block_mut(i, false).await.expect("edit").expect("mapped");
blk.as_mut_slice().fill(0xBB);
}
let _ = h.fs_flush().await.expect("flush 2");
let _ = h.fs_release().await.expect("release");
}
let mut h = open_rdwr(&client, tf.uri()).await;
for i in 0..N {
let mut buf = vec![0u8; BLK];
let _ = h.fs_read(i as usize * BLK, &mut buf).await.expect("read");
assert!(buf.iter().all(|b| *b == 0xBB),
"write-then-block_mut: block {i} lost the edit");
}
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
{
let tf = TestFile::new(&client).await;
{
let mut h = open_rdwr(&client, tf.uri()).await;
for i in 0..N {
let mut blk = h.fs_block_mut(i, true).await.expect("create").expect("created");
blk.as_mut_slice().fill(0xAA);
}
let _ = h.fs_truncate(N as usize * BLK).await.expect("truncate");
let _ = h.fs_flush().await.expect("flush 1");
for i in 0..N {
let _ = h.fs_write(i as usize * BLK, &vec![0xBBu8; BLK]).await.expect("write");
}
let _ = h.fs_flush().await.expect("flush 2");
let _ = h.fs_release().await.expect("release");
}
let mut h = open_rdwr(&client, tf.uri()).await;
for i in 0..N {
let blk = h.fs_block(i).await.expect("cold").expect("mapped");
assert_eq!(blk.as_slice()[0], 0xBB,
"block_mut-then-write: block {i} lost the edit");
}
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}
}
#[tokio::test]
#[ignore]
async fn block_mut_updates_mtime() {
let _ = env_logger::try_init();
let client = make_client().await;
let tf = TestFile::new(&client).await;
let mut h = open_rdwr(&client, tf.uri()).await;
{
let mut blk = h.fs_block_mut(0, true).await.expect("create").expect("created");
blk.as_mut_slice().fill(0x01);
}
let _ = h.fs_flush().await.expect("flush");
let before = h.fs_getattr().expect("getattr");
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
{
let mut blk = h.fs_block_mut(0, false).await.expect("edit").expect("mapped");
blk.as_mut_slice().fill(0x02);
}
let after = h.fs_getattr().expect("getattr");
assert!(after.st_mtime > before.st_mtime,
"an in-place edit must move mtime: {} -> {}", before.st_mtime, after.st_mtime);
let _ = h.fs_flush().await.expect("flush");
let _ = h.fs_release().await.expect("release");
tf.cleanup(&client).await;
}