use std::assert_matches;
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use fs_err as fs;
use super::pipeline::DiskCachePipeline;
use super::{
BLOCK_SIZE, DiskCache, DiskCacheConfig, DiskCacheFs, DiskCacheFsContext, DiskCacheRemote,
};
use crate::common::generic_consts::{Random, Sequential};
use crate::common::mmap::AdviceSetting;
use crate::common::universal_io::cached_fs::FileInfo;
use crate::common::universal_io::{
CachedFs, CachedReadFs, MmapFile, OpenOptions, Populate, ReadPipeline, ReadRange,
UniversalAppend, UniversalFlush, UniversalIoError, UniversalRead, UniversalReadFileOps,
UniversalReadFs, UniversalWrite, UniversalWriteFileOps,
};
static_assertions::assert_not_impl_any!(
DiskCache<MmapFile>: UniversalAppend, UniversalFlush, UniversalWrite
);
static_assertions::assert_not_impl_any!(DiskCacheFs<MmapFile>: UniversalWriteFileOps);
fn make_test_data(n_bytes: usize) -> Vec<u8> {
(0..n_bytes).map(|i| (i % 251) as u8).collect()
}
struct Scenario {
_tmp: tempfile::TempDir,
remote_path: PathBuf,
data: Vec<u8>,
config: Arc<DiskCacheConfig>,
}
impl Scenario {
fn new(n_bytes: usize) -> Self {
let tmp = tempfile::Builder::new()
.prefix("disk_cache_tests")
.tempdir()
.unwrap();
let remote_dir = tmp.path().join("remote");
let local_dir = tmp.path().join("local");
fs::create_dir_all(&remote_dir).unwrap();
fs::create_dir_all(&local_dir).unwrap();
let remote_path = remote_dir.join("data.bin");
let data = make_test_data(n_bytes);
fs::write(&remote_path, &data).unwrap();
Self {
_tmp: tmp,
remote_path,
data,
config: Arc::new(DiskCacheConfig::new(remote_dir, local_dir).unwrap()),
}
}
fn local_path_base(&self) -> PathBuf {
self.config.local_path_for(&self.remote_path).unwrap()
}
fn fs<R>(&self) -> DiskCacheFs<R>
where
R: DiskCacheRemote,
<R::Fs as UniversalReadFileOps>::ContextConfig: Default,
{
DiskCacheFs::<R>::from_context(DiskCacheFsContext {
config: self.config.clone(),
remote: Default::default(),
})
.unwrap()
}
fn open<R>(&self, prefill: bool) -> DiskCache<R>
where
R: DiskCacheRemote,
<R::Fs as UniversalReadFileOps>::ContextConfig: Default,
{
let populate = if prefill {
Populate::PreferBackground
} else {
Populate::No
};
self.fs()
.open(
&self.remote_path,
OpenOptions {
writeable: false,
populate,
need_sequential: false,
advice: AdviceSetting::Global,
},
Default::default(),
)
.unwrap()
}
fn open_partial<R>(&self, range: std::ops::Range<u64>) -> DiskCache<R>
where
R: DiskCacheRemote,
<R::Fs as UniversalReadFileOps>::ContextConfig: Default,
{
let fs = DiskCacheFs::<R>::from_context(DiskCacheFsContext {
config: self.config.clone(),
remote: Default::default(),
})
.unwrap();
fs.open(
&self.remote_path,
OpenOptions {
writeable: false,
populate: Populate::Partial(ReadRange::new(range.start, range.end - range.start)),
need_sequential: false,
advice: AdviceSetting::Global,
},
Default::default(),
)
.unwrap()
}
fn slice(&self, range: &std::ops::Range<u64>) -> &[u8] {
&self.data[range.start as usize..range.end as usize]
}
fn snapshot_file_info<R>(&self) -> impl Fn(&Path) -> Option<FileInfo>
where
R: DiskCacheRemote,
<R::Fs as UniversalReadFileOps>::ContextConfig: Default,
{
let mut cached_fs = CachedFs::new(self.fs::<R>(), &self.remote_path).unwrap();
cached_fs.cache_file_info().unwrap();
move |path| cached_fs.file_info(path).cloned()
}
fn grow_remote(&mut self, additional_bytes: usize) -> Vec<u8> {
use std::io::Write;
let old_len = self.data.len();
let new_data = make_test_data(old_len + additional_bytes);
let mut file = fs::OpenOptions::new()
.append(true)
.open(&self.remote_path)
.unwrap();
file.write_all(&new_data[old_len..]).unwrap();
self.data = new_data.clone();
new_data
}
}
fn drain_pipeline<R: DiskCacheRemote>(
pipeline: &mut DiskCachePipeline<'_, R, u32>,
) -> HashMap<u32, Vec<u8>> {
let mut results = HashMap::new();
while let Some((user_data, bytes)) = pipeline.wait().unwrap() {
let previous = results.insert(user_data, bytes.to_vec());
assert!(previous.is_none(), "duplicate result for {user_data}");
}
results
}
#[duplicate::duplicate_item(
tests_mod R cfg_predicate _PREFILL;
[tests_prefill] [MmapFile] [cfg(all())] [true];
[tests_mmap] [MmapFile] [cfg(all())] [false];
[tests_uring] [IoUringFile] [cfg(target_os = "linux")] [false];
)]
#[cfg_predicate]
#[cfg(test)]
mod tests_mod {
use std::sync::atomic::Ordering;
use super::*;
#[cfg_predicate]
use crate::common::universal_io::R;
use crate::common::universal_io::UioResult;
const PREFILL: bool = _PREFILL;
#[test]
fn basic_read_returns_remote_bytes() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(PREFILL);
let bytes = file
.read::<_, u8>(ReadRange::new(10, 20), Sequential)
.unwrap();
assert_eq!(&*bytes, &scn.data[10..30]);
let last = scn.data.len() as u64;
let bytes = file
.read::<_, u8>(ReadRange::new(last - 50, 50), Sequential)
.unwrap();
assert_eq!(&*bytes, &scn.data[scn.data.len() - 50..]);
}
#[test]
fn read_whole_empty_remote_returns_empty() {
let scn = Scenario::new(0);
let file = scn.open::<R>(PREFILL);
let bytes = file.read_whole::<u8>().unwrap();
assert!(bytes.is_empty());
assert_eq!(file.len::<u8>().unwrap(), 0);
}
#[test]
fn read_spanning_multiple_blocks_is_contiguous() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(PREFILL);
let start = (BLOCK_SIZE - 50) as u64;
let len = (BLOCK_SIZE + 100) as u64;
let bytes = file
.read::<_, u8>(ReadRange::new(start, len), Sequential)
.unwrap();
let start = start as usize;
let end = start + len as usize;
assert_matches!(bytes, Cow::Borrowed(_));
assert_eq!(bytes.as_ref(), &scn.data[start..end]);
}
#[test]
fn local_file_is_created_on_first_read() {
let scn = Scenario::new(BLOCK_SIZE * 2);
let file = scn.open::<R>(PREFILL);
let expected_local = file.local_path.clone();
assert!(
expected_local
.to_str()
.unwrap()
.starts_with(scn.local_path_base().to_str().unwrap()),
"unique mirror name must derive from the configured mapping",
);
assert!(
!expected_local.exists(),
"local file should not exist before first read: {}",
expected_local.display(),
);
let _ = file.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
assert!(
expected_local.exists(),
"local file should exist after first read"
);
assert_eq!(
fs::metadata(&expected_local).unwrap().len(),
scn.data.len() as u64,
"local file should be sized to the remote",
);
}
#[test]
fn populate_fetches_every_block() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(PREFILL);
file.populate().unwrap();
let bytes = file
.read::<_, u8>(ReadRange::new(0, scn.data.len() as u64), Sequential)
.unwrap();
assert_eq!(&*bytes, &scn.data[..]);
}
#[test]
fn read_past_end_returns_out_of_bounds() {
let scn = Scenario::new(1024);
let file = scn.open::<R>(PREFILL);
let err = file
.read::<_, u8>(ReadRange::new(1000, 100), Sequential)
.unwrap_err();
assert_matches!(
err,
crate::common::universal_io::UniversalIoError::OutOfBounds { .. },
);
}
#[test]
fn concurrent_instances_have_independent_mirrors() {
let scn = Scenario::new(BLOCK_SIZE * 2);
let first = scn.open::<R>(PREFILL);
let read_all = |cache: &DiskCache<R>| {
cache
.read::<_, u8>(ReadRange::new(0, scn.data.len() as u64), Sequential)
.unwrap()
.to_vec()
};
assert_eq!(read_all(&first), scn.data);
let second = scn.open::<R>(PREFILL);
assert_ne!(first.local_path, second.local_path);
assert_eq!(read_all(&second), scn.data);
assert_eq!(read_all(&first), scn.data);
}
#[test]
fn drop_removes_local_mirror() {
let scn = Scenario::new(BLOCK_SIZE);
let cache = scn.open::<R>(PREFILL);
let _ = cache.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
let local_path = cache.local_path.clone();
assert!(local_path.exists());
drop(cache);
assert!(!local_path.exists());
}
#[test]
fn reopen_no_growth_does_not_repopulate() {
let scn = Scenario::new(BLOCK_SIZE * 3);
let mut cache = scn.open::<R>(PREFILL);
let _ = cache.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
let (len_before, populated_before, fetched_before) = {
let local = cache.state().expect("local initialized after read").local;
(
local.mmap().len::<u8>().unwrap(),
local.fully_populated.load(Ordering::Acquire),
local.fetched.lock().clone(),
)
};
cache.reopen().unwrap();
let local = if PREFILL {
cache.state().unwrap().local
} else {
assert!(cache.is_ready(), "local must still be initialized");
cache.state().unwrap().local
};
assert_eq!(local.mmap().len::<u8>().unwrap(), len_before);
assert_eq!(
local.fully_populated.load(Ordering::Acquire),
populated_before,
);
assert_eq!(local.fetched.lock().clone(), fetched_before);
}
#[test]
fn reopen_growth_visible_after_reopen() {
let mut scn = Scenario::new(BLOCK_SIZE * 2);
let mut cache = scn.open::<R>(PREFILL);
let original_len = scn.data.len() as u64;
let _ = cache.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
let new_data = scn.grow_remote(BLOCK_SIZE);
let err = cache
.read::<_, u8>(ReadRange::new(original_len, BLOCK_SIZE as u64), Sequential)
.unwrap_err();
assert_matches!(
err,
crate::common::universal_io::UniversalIoError::OutOfBounds { .. },
);
cache.reopen().unwrap();
let bytes = cache
.read::<_, u8>(ReadRange::new(original_len, BLOCK_SIZE as u64), Sequential)
.unwrap();
assert_eq!(&*bytes, &new_data[original_len as usize..]);
}
#[test]
fn reopen_growth_refetches_partial_tail_block() {
let mut scn = Scenario::new(BLOCK_SIZE + 100);
let mut cache = scn.open::<R>(PREFILL);
let _ = cache
.read::<_, u8>(ReadRange::one(BLOCK_SIZE as u64), Sequential)
.unwrap();
let new_data = scn.grow_remote(BLOCK_SIZE);
cache.reopen().unwrap();
let bytes = cache
.read::<_, u8>(
ReadRange::new(BLOCK_SIZE as u64, BLOCK_SIZE as u64),
Sequential,
)
.unwrap();
assert_eq!(&*bytes, &new_data[BLOCK_SIZE..BLOCK_SIZE * 2]);
}
#[test]
fn reopen_schedule_is_invisible_until_reopen() {
let mut scn = Scenario::new(BLOCK_SIZE * 2);
let mut cache = scn.open::<R>(PREFILL);
let original_len = scn.data.len() as u64;
let _ = cache.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
let new_data = scn.grow_remote(BLOCK_SIZE);
cache
.schedule_reopen(scn.snapshot_file_info::<R>())
.unwrap();
assert_eq!(cache.len::<u8>().unwrap(), original_len);
let err = cache
.read::<_, u8>(ReadRange::new(original_len, BLOCK_SIZE as u64), Sequential)
.unwrap_err();
assert_matches!(err, UniversalIoError::OutOfBounds { .. });
cache.reopen().unwrap();
assert_eq!(cache.len::<u8>().unwrap(), new_data.len() as u64);
let new_block = (original_len / BLOCK_SIZE as u64) as u32;
assert_eq!(
cache
.state()
.unwrap()
.local
.contains(new_block..new_block + 1),
PREFILL,
);
let bytes = cache
.read::<_, u8>(ReadRange::new(original_len, BLOCK_SIZE as u64), Sequential)
.unwrap();
assert_eq!(&*bytes, &new_data[original_len as usize..]);
}
#[test]
fn reopen_schedule_materializes_cold_mirror() {
let scn = Scenario::new(BLOCK_SIZE * 2 + 100);
let mut cache = scn.open::<R>(PREFILL);
assert!(!cache.is_ready());
cache
.schedule_reopen(scn.snapshot_file_info::<R>())
.unwrap();
assert!(cache.is_ready());
assert_eq!(cache.len::<u8>().unwrap(), scn.data.len() as u64);
cache.reopen().unwrap();
assert_eq!(cache.len::<u8>().unwrap(), scn.data.len() as u64);
let bytes = cache.read_whole::<u8>().unwrap();
assert_eq!(&*bytes, &scn.data[..]);
}
#[test]
fn reopen_schedule_no_growth_does_not_repopulate() {
let scn = Scenario::new(BLOCK_SIZE * 3);
let mut cache = scn.open::<R>(PREFILL);
let _ = cache.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
let (len_before, fetched_before) = {
let local = cache.state().unwrap().local;
(
local.mmap().len::<u8>().unwrap(),
local.fetched.lock().clone(),
)
};
cache
.schedule_reopen(scn.snapshot_file_info::<R>())
.unwrap();
cache.reopen().unwrap();
let local = cache.state().unwrap().local;
assert_eq!(local.mmap().len::<u8>().unwrap(), len_before);
assert_eq!(*local.fetched.lock(), fetched_before);
}
#[test]
fn reopen_schedule_twice_without_apply() {
let mut scn = Scenario::new(BLOCK_SIZE * 2);
let mut cache = scn.open::<R>(PREFILL);
let _ = cache.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
scn.grow_remote(BLOCK_SIZE);
cache
.schedule_reopen(scn.snapshot_file_info::<R>())
.unwrap();
let new_data = scn.grow_remote(BLOCK_SIZE);
cache
.schedule_reopen(scn.snapshot_file_info::<R>())
.unwrap();
cache.reopen().unwrap();
assert_eq!(cache.len::<u8>().unwrap(), new_data.len() as u64);
let bytes = cache.read_whole::<u8>().unwrap();
assert_eq!(&*bytes, &new_data[..]);
}
#[test]
fn reopen_schedule_twice_with_same_length() {
let mut scn = Scenario::new(BLOCK_SIZE * 2);
let mut cache = scn.open::<R>(PREFILL);
let _ = cache.read::<_, u8>(ReadRange::one(0), Sequential).unwrap();
let new_data = scn.grow_remote(BLOCK_SIZE);
let get_file_info = scn.snapshot_file_info::<R>();
cache.schedule_reopen(&get_file_info).unwrap();
cache.schedule_reopen(&get_file_info).unwrap();
cache.reopen().unwrap();
assert_eq!(cache.len::<u8>().unwrap(), new_data.len() as u64);
let bytes = cache.read_whole::<u8>().unwrap();
assert_eq!(&*bytes, &new_data[..]);
}
#[test]
fn reopen_schedule_missing_from_snapshot_errors() {
let scn = Scenario::new(BLOCK_SIZE);
let mut cache = scn.open::<R>(PREFILL);
let err = cache.schedule_reopen(|_| None).unwrap_err();
assert_matches!(err, UniversalIoError::NotFound { .. });
}
#[test]
fn partial_populate_fetches_only_requested_range() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open_partial::<R>(0..(BLOCK_SIZE as u64 + 50));
let expected_local = file.local_path.clone();
assert!(!expected_local.exists());
assert!(!file.is_ready());
let bytes = file
.read::<_, u8>(ReadRange::new(10, 20), Sequential)
.unwrap();
assert_eq!(&*bytes, &scn.data[10..30]);
assert!(expected_local.exists());
{
let local = file.state().unwrap().local;
assert!(!local.fully_populated.load(Ordering::Acquire));
assert!(local.fetched.lock().contains_range(0..2));
assert!(!local.fetched.lock().contains(2));
assert!(!local.fetched.lock().contains(3));
}
let start = (BLOCK_SIZE * 2) as u64;
let bytes = file
.read::<_, u8>(ReadRange::new(start, 30), Sequential)
.unwrap();
assert_eq!(&*bytes, &scn.data[start as usize..start as usize + 30]);
assert!(file.state().unwrap().local.fetched.lock().contains(2));
}
#[test]
fn partial_populate_empty_range_is_lazy() {
let scn = Scenario::new(BLOCK_SIZE * 2 + 100);
let file = scn.open_partial::<R>(10..10);
let bytes = file
.read::<_, u8>(ReadRange::new(0, 16), Sequential)
.unwrap();
assert_eq!(&*bytes, &scn.data[0..16]);
assert_eq!(file.len::<u8>().unwrap(), scn.data.len() as u64);
}
#[test]
fn partial_populate_range_past_eof_is_lazy() {
let scn = Scenario::new(100);
let file = scn.open_partial::<R>(BLOCK_SIZE as u64 * 4..BLOCK_SIZE as u64 * 5);
let bytes = file
.read::<_, u8>(ReadRange::new(0, 100), Sequential)
.unwrap();
assert_eq!(&*bytes, &scn.data[..]);
assert_eq!(file.len::<u8>().unwrap(), 100);
}
#[test]
fn same_block_reads_share_one_fetch() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(false);
let mut pipeline = DiskCachePipeline::<R, u32>::new().unwrap();
pipeline.schedule::<Random>(0, &file, 10..30, 1).unwrap();
pipeline.schedule::<Random>(1, &file, 100..200, 1).unwrap();
assert_eq!(pipeline.in_flight_fetches(), 1);
let results = drain_pipeline(&mut pipeline);
assert_eq!(results.len(), 2);
assert_eq!(results[&0], &scn.data[10..30]);
assert_eq!(results[&1], &scn.data[100..200]);
}
#[test]
fn spanning_fetch_covers_contained_reads() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(false);
let mut pipeline = DiskCachePipeline::<R, u32>::new().unwrap();
let spanning = (BLOCK_SIZE - 50) as u64..(BLOCK_SIZE + 50) as u64;
let contained = (BLOCK_SIZE + 100) as u64..(BLOCK_SIZE + 200) as u64;
pipeline
.schedule::<Random>(0, &file, spanning.clone(), 1)
.unwrap();
pipeline
.schedule::<Random>(1, &file, contained.clone(), 1)
.unwrap();
assert_eq!(pipeline.in_flight_fetches(), 1);
let results = drain_pipeline(&mut pipeline);
assert_eq!(results[&0], scn.slice(&spanning));
assert_eq!(results[&1], scn.slice(&contained));
}
#[test]
fn multi_block_reads_share_one_fetch() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(false);
let eof = scn.data.len() as u64;
let mut pipeline = DiskCachePipeline::<R, u32>::new().unwrap();
let spanning = 100u64..eof - 10;
let middle = (BLOCK_SIZE + 200) as u64..(BLOCK_SIZE * 2 + 200) as u64;
let tail = (BLOCK_SIZE * 3 - 50) as u64..eof;
pipeline
.schedule::<Random>(0, &file, spanning.clone(), 1)
.unwrap();
pipeline
.schedule::<Random>(1, &file, middle.clone(), 1)
.unwrap();
pipeline
.schedule::<Random>(2, &file, tail.clone(), 1)
.unwrap();
assert_eq!(pipeline.in_flight_fetches(), 1);
let results = drain_pipeline(&mut pipeline);
assert_eq!(results[&0], scn.slice(&spanning));
assert_eq!(results[&1], scn.slice(&middle));
assert_eq!(results[&2], scn.slice(&tail));
}
#[test]
fn partially_covered_read_does_not_piggyback() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(false);
let mut pipeline = DiskCachePipeline::<R, u32>::new().unwrap();
let first = (BLOCK_SIZE - 50) as u64..(BLOCK_SIZE + 50) as u64;
let second = (BLOCK_SIZE + 100) as u64..(BLOCK_SIZE * 2 + 100) as u64;
pipeline
.schedule::<Random>(0, &file, first.clone(), 1)
.unwrap();
let mut results = HashMap::new();
match pipeline.schedule::<Random>(1, &file, second.clone(), 1) {
Ok(()) => assert_eq!(pipeline.in_flight_fetches(), 2),
Err(UniversalIoError::QueueIsFull) => {
assert_eq!(pipeline.in_flight_fetches(), 1);
results.extend(drain_pipeline(&mut pipeline));
pipeline
.schedule::<Random>(1, &file, second.clone(), 1)
.unwrap();
assert_eq!(pipeline.in_flight_fetches(), 1);
}
Err(err) => panic!("unexpected error: {err}"),
}
results.extend(drain_pipeline(&mut pipeline));
assert_eq!(results[&0], scn.slice(&first));
assert_eq!(results[&1], scn.slice(&second));
}
#[test]
fn different_files_do_not_share_fetches() {
let scn = Scenario::new(BLOCK_SIZE * 2);
let file_a = scn.open::<R>(false);
let file_b = scn.open::<R>(false);
let mut pipeline = DiskCachePipeline::<R, u32>::new().unwrap();
pipeline.schedule::<Random>(0, &file_a, 10..30, 1).unwrap();
match pipeline.schedule::<Random>(1, &file_b, 10..30, 1) {
Ok(()) => assert_eq!(pipeline.in_flight_fetches(), 2),
Err(UniversalIoError::QueueIsFull) => {
assert_eq!(pipeline.in_flight_fetches(), 1);
}
Err(err) => panic!("unexpected error: {err}"),
}
let results = drain_pipeline(&mut pipeline);
assert_eq!(results[&0], &scn.data[10..30]);
}
#[test]
fn committed_blocks_serve_later_reads_locally() {
let scn = Scenario::new(BLOCK_SIZE * 2);
let file = scn.open::<R>(false);
let mut pipeline = DiskCachePipeline::<R, u32>::new().unwrap();
pipeline.schedule::<Random>(0, &file, 10..30, 1).unwrap();
let results = drain_pipeline(&mut pipeline);
assert_eq!(results[&0], &scn.data[10..30]);
pipeline.schedule::<Random>(1, &file, 40..60, 1).unwrap();
assert_eq!(pipeline.in_flight_fetches(), 0);
let results = drain_pipeline(&mut pipeline);
assert_eq!(results[&1], &scn.data[40..60]);
}
#[test]
fn read_batch_with_shared_blocks_resolves_every_read() {
let scn = Scenario::new(BLOCK_SIZE * 3 + 100);
let file = scn.open::<R>(false);
let ranges: Vec<(usize, ReadRange)> = (0..64)
.map(|i| {
let range = ReadRange {
byte_offset: (i * 700) as u64,
length: 100,
};
(i, range)
})
.collect();
let mut seen = vec![false; ranges.len()];
file.read_batch(ranges.clone(), Random, |i, bytes: &[u8]| {
let start = ranges[i].1.byte_offset as usize;
assert_eq!(bytes, &scn.data[start..start + 100]);
assert!(!seen[i]);
seen[i] = true;
UioResult::Ok(())
})
.unwrap();
assert!(seen.iter().all(|&s| s));
}
#[test]
fn writeable_open_is_rejected() {
let scn = Scenario::new(10);
let err = scn
.fs::<R>()
.open(
&scn.remote_path,
OpenOptions {
writeable: true,
populate: Populate::No,
need_sequential: false,
advice: AdviceSetting::Global,
},
Default::default(),
)
.unwrap_err();
assert_matches!(
err,
crate::common::universal_io::UniversalIoError::Uninitialized { .. },
);
}
}