#![cfg(feature = "fault-injection")]
use std::time::Duration;
use aws_sdk_s3::Client;
use hyperdir::fault::S3FaultInterceptor;
use hyperdir::hyper::HyperDir;
use hyperdir::HyperDirLayout;
use hyperfile::file::flags::FileFlags;
use hyperfile::file::mode::FileMode;
use hyperfile::inode::Inode;
use uuid::Uuid;
async fn client_with(fault: Option<S3FaultInterceptor>) -> Client {
let region = std::env::var("S3_REGION").expect("S3_REGION not set");
let cfg = aws_config::from_env().region(aws_config::Region::new(region)).load().await;
let mut b = aws_sdk_s3::config::Builder::from(&cfg).interceptor(hyperdir::S3ClockInterceptor);
if let Some(f) = fault {
b = b.interceptor(f);
}
b = b.retry_config(aws_sdk_s3::config::retry::RetryConfig::disabled());
Client::from_conf(b.build())
}
async fn purge(client: &Client, bucket: &str, prefix: &str) {
let mut stream = client.list_objects_v2().bucket(bucket).prefix(format!("{prefix}/")).into_paginator().send();
let mut keys = Vec::new();
while let Some(page) = stream.next().await {
let Ok(p) = page else { break };
for o in p.contents() {
if let Some(k) = o.key() { keys.push(k.to_string()); }
}
}
for k in keys {
let _ = client.delete_object().bucket(bucket).key(k).send().await;
}
}
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn e2e_fault_resolve_errors_when_listing_fails() {
let (Ok(bkt), Ok(_)) = (std::env::var("S3_BUCKET"), std::env::var("S3_REGION")) else {
eprintln!("skipping: S3_BUCKET/S3_REGION not set");
return;
};
let fs = format!("hyperdir-fault/{}", Uuid::new_v4());
let layout = HyperDirLayout::with_base(&fs);
let clean = client_with(None).await;
HyperDir::fs_create_root(&clean, &layout, &bkt, FileFlags::rdwr(), FileMode::from(0o755))
.await
.expect("create root");
let root = Uuid::nil();
let mut inl = Inode::default_file().to_raw(unsafe { std::mem::zeroed() });
inl.i_mode = libc::S_IFREG | 0o644;
inl.set_inline(b"hello");
let uuid = HyperDir::fs_create_inline(&clean, &layout, &bkt, &root, "f", &inl)
.await
.expect("create inline");
assert!(HyperDir::fs_resolve_entry_fast(&clean, &layout, &bkt, &root, "f").await.unwrap().is_some(),
"present before unlink");
HyperDir::fs_unlink(&clean, &layout, &bkt, &root, "f", &uuid, false, None)
.await
.expect("unlink");
assert!(HyperDir::fs_resolve_entry_fast(&clean, &layout, &bkt, &root, "f").await.unwrap().is_none(),
"absent after unlink (no faults)");
let fault = S3FaultInterceptor::new(&["ListObjectsV2"], 1, 0);
let faulty = client_with(Some(fault.clone())).await;
let res = HyperDir::fs_resolve_entry_fast(&faulty, &layout, &bkt, &root, "f").await;
assert!(fault.injected() > 0, "the fault interceptor must have fired");
match res {
Err(e) => eprintln!(" resolve surfaced the listing failure as expected: {e}"),
Ok(Some(_)) => panic!("resolve resurrected a deleted name from a partial listing"),
Ok(None) => panic!("resolve reported a definitive answer despite a failed listing"),
}
assert!(HyperDir::fs_resolve_entry_fast(&clean, &layout, &bkt, &root, "f").await.unwrap().is_none(),
"still absent once listing works again");
purge(&clean, &bkt, &fs).await;
let _ = Duration::from_secs(0);
}