use std::sync::Arc;
use std::sync::atomic::Ordering;
#[cfg(feature = "async")]
use qubit_fs::AsyncFileSystem;
use qubit_fs::FileSystem;
use qubit_fs::FsError;
use qubit_fs::Path;
use qubit_fs::directory::ListFilter;
use qubit_fs::directory::ListOptions;
use qubit_fs::directory::ListScope;
use qubit_fs::error::FsErrorKind;
use qubit_fs::error::FsOperation;
use qubit_fs::metadata::DirEntry;
use qubit_fs::metadata::FileKind;
use qubit_fs::metadata::FileSystemLimit;
use qubit_fs::metadata::FileSystemLimits;
use qubit_fs::path::PathSemantics;
mod support;
use support::listing::ListingProvider;
use support::listing::Observations;
#[cfg(feature = "async")]
mod common;
#[test]
fn namespace_is_not_an_empty_resource_path() {
assert!(Path::parse_literal("").is_err());
assert!(ListScope::Namespace.path().is_none());
let path = Path::parse_literal("folder/").unwrap();
let scope = ListScope::Path(path.clone());
assert_eq!(scope.path(), Some(&path));
}
fn check_listing(
scope: ListScope,
options: ListOptions,
semantics: PathSemantics,
supported: bool,
limits: FileSystemLimits,
entries: Vec<DirEntry>,
expected: Result<(), (FsErrorKind, bool)>,
) {
let modes = if cfg!(feature = "async") {
&[false, true][..]
} else {
&[false][..]
};
for &asynchronous in modes {
let observations = Arc::new(Observations::default());
let provider = ListingProvider {
semantics,
supported,
limits,
entries: entries.clone(),
observations: Arc::clone(&observations),
};
let mut actual = Vec::new();
let result = if asynchronous {
#[cfg(feature = "async")]
{
common::poll_support::ready(async {
let filesystem = AsyncFileSystem::from_spi(provider).unwrap();
let mut stream = filesystem.list(&scope, options.clone()).await?;
while let Some(entry) = stream.next_entry_async().await? {
actual.push(entry.path);
}
Ok::<_, FsError>(())
})
}
#[cfg(not(feature = "async"))]
{
unreachable!("async feature required")
}
} else {
(|| {
let filesystem = FileSystem::from_spi(provider).unwrap();
let mut stream = filesystem.list(&scope, options.clone())?;
while let Some(entry) = stream.next_entry()? {
actual.push(entry.path);
}
Ok::<_, FsError>(())
})()
};
match expected {
Ok(()) => {
result.unwrap();
assert_eq!(
actual,
entries.iter().map(|entry| entry.path.clone()).collect::<Vec<_>>()
);
assert_eq!(observations.list_calls.load(Ordering::SeqCst), 1);
assert_eq!(observations.requests.lock().unwrap()[0].0, scope);
}
Err((kind, opened)) => {
let error = result.unwrap_err();
assert_eq!(error.kind(), kind);
assert_eq!(error.operation(), FsOperation::List);
if !opened {
assert_eq!(error.path(), scope.path());
}
assert_eq!(observations.list_calls.load(Ordering::SeqCst), usize::from(opened));
}
}
}
}
fn keys(names: &[&str]) -> Vec<DirEntry> {
names
.iter()
.map(|name| DirEntry::new(Path::parse_literal(name).unwrap(), FileKind::File))
.collect()
}
#[test]
fn namespace_accepts_entire_flat_keyspace() {
check_listing(
ListScope::Namespace,
ListOptions::object_keys(),
PathSemantics::ObjectKey,
true,
FileSystemLimits::unknown(),
keys(&["a", "b/c"]),
Ok(()),
);
}
#[test]
fn raw_prefix_and_relative_filter_preserve_exact_text() {
check_listing(
ListScope::Path(Path::parse_literal("folder").unwrap()),
ListOptions::object_keys(),
PathSemantics::ObjectKey,
true,
FileSystemLimits::unknown(),
keys(&["folder/a", "folderish"]),
Ok(()),
);
let options = ListOptions::object_keys().with_filter(Some(ListFilter::LiteralPrefix("a".into())));
check_listing(
ListScope::Path(Path::parse_literal("folder/").unwrap()),
options.clone(),
PathSemantics::ObjectKey,
true,
FileSystemLimits::unknown(),
keys(&["folder/a", "folder/ab"]),
Ok(()),
);
check_listing(
ListScope::Namespace,
options.clone(),
PathSemantics::ObjectKey,
true,
FileSystemLimits::unknown(),
keys(&["a", "ab"]),
Ok(()),
);
check_listing(
ListScope::Namespace,
options,
PathSemantics::ObjectKey,
true,
FileSystemLimits::unknown(),
keys(&["ba"]),
Err((FsErrorKind::ProviderContractViolation, true)),
);
}
#[test]
fn namespace_preflight_rejects_hierarchical_and_unsupported_listing() {
check_listing(
ListScope::Namespace,
ListOptions::default(),
PathSemantics::Hierarchical,
true,
FileSystemLimits::unknown(),
vec![],
Err((FsErrorKind::InvalidOptions, false)),
);
check_listing(
ListScope::Namespace,
ListOptions::object_keys(),
PathSemantics::ObjectKey,
false,
FileSystemLimits::unknown(),
vec![],
Err((FsErrorKind::UnsupportedCapability, false)),
);
check_listing(
ListScope::Path(Path::root()),
ListOptions::default(),
PathSemantics::Hierarchical,
true,
FileSystemLimits::unknown(),
vec![DirEntry::new(Path::parse("/a").unwrap(), FileKind::File)],
Ok(()),
);
}
#[test]
fn query_and_entry_limits_are_distinct() {
let limits = FileSystemLimits::unknown().with_max_path_text_bytes(FileSystemLimit::Maximum(4));
let options = ListOptions::object_keys().with_filter(Some(ListFilter::LiteralPrefix("abcde".into())));
check_listing(
ListScope::Namespace,
options,
PathSemantics::ObjectKey,
true,
limits,
vec![],
Err((FsErrorKind::ResourceLimitExceeded, false)),
);
let options = ListOptions::object_keys().with_filter(Some(ListFilter::LiteralPrefix("cde".into())));
check_listing(
ListScope::Path(Path::parse_literal("ab").unwrap()),
options,
PathSemantics::ObjectKey,
true,
limits,
vec![],
Err((FsErrorKind::ResourceLimitExceeded, false)),
);
check_listing(
ListScope::Namespace,
ListOptions::object_keys(),
PathSemantics::ObjectKey,
true,
limits,
keys(&["abcde"]),
Err((FsErrorKind::ProviderContractViolation, true)),
);
}
#[test]
fn namespace_rejects_foreign_entry_semantics() {
check_listing(
ListScope::Namespace,
ListOptions::object_keys(),
PathSemantics::ObjectKey,
true,
FileSystemLimits::unknown(),
vec![DirEntry::new(Path::parse("/a").unwrap(), FileKind::File)],
Err((FsErrorKind::ProviderContractViolation, true)),
);
}