use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use qubit_fs::FileSystem;
use qubit_fs::FsError;
use qubit_fs::FsResult;
use qubit_fs::Path;
use qubit_fs::copy::CopyOptions;
use qubit_fs::directory::CreateDirectoryOptions;
use qubit_fs::directory::CreateDirectoryOutcome;
use qubit_fs::directory::DeleteOptions;
use qubit_fs::directory::DeleteOutcome;
use qubit_fs::directory::ListOptions;
use qubit_fs::directory::ListScope;
use qubit_fs::error::FsErrorKind;
use qubit_fs::error::FsOperation;
use qubit_fs::metadata::AchievedAtomicity;
use qubit_fs::metadata::FileKind;
use qubit_fs::metadata::FileMetadata;
use qubit_fs::metadata::FileSystemCapabilities;
use qubit_fs::metadata::FileSystemCapability;
use qubit_fs::metadata::FileSystemId;
use qubit_fs::metadata::FileSystemInfo;
use qubit_fs::metadata::FileSystemLimits;
use qubit_fs::metadata::PublicationMethod;
use qubit_fs::metadata::SymlinkPolicy;
use qubit_fs::path::PathConstraints;
use qubit_fs::path::PathSemantics;
use qubit_fs::read::ReadOptions;
use qubit_fs::rename::RenameFailureState;
use qubit_fs::rename::RenameOptions;
use qubit_fs::rename::RenameOutcome;
use qubit_fs::spi::CreateDirectoryRequest;
use qubit_fs::spi::CreateTempDirectoryRequest;
use qubit_fs::spi::CreateTempFileRequest;
use qubit_fs::spi::DeleteDirectoryRequest;
use qubit_fs::spi::DeleteFileRequest;
use qubit_fs::spi::FileSystemSpi;
use qubit_fs::spi::ListRequest;
use qubit_fs::spi::OpenReaderRequest;
use qubit_fs::spi::OpenWriterRequest;
use qubit_fs::spi::OpenedDirectoryStream;
use qubit_fs::spi::OpenedReader;
use qubit_fs::spi::OpenedTempDirectory;
use qubit_fs::spi::OpenedTempFile;
use qubit_fs::spi::OpenedWriter;
use qubit_fs::spi::ProviderOperation;
use qubit_fs::spi::ProviderOperations;
use qubit_fs::spi::ProviderProperties;
use qubit_fs::spi::RenameRequest;
use qubit_fs::spi::SpiRenameFailure;
use qubit_fs::spi::StatRequest;
use qubit_fs::spi::StatResponse;
use qubit_fs::temp::TempOptions;
use qubit_fs::write::WriteOptions;
struct CountingSpi {
properties: ProviderProperties,
property_calls: Arc<AtomicUsize>,
stat_calls: Arc<AtomicUsize>,
wrong_stat_path: bool,
stat_error: Option<FsErrorKind>,
direct_error: bool,
unexpected_create: bool,
unexpected_delete: bool,
}
impl CountingSpi {
fn unsupported() -> FsError {
FsError::new(
FsErrorKind::UnsupportedOperation,
FsOperation::Other,
"unused test operation",
)
}
}
impl FileSystemSpi for CountingSpi {
fn properties(&self) -> ProviderProperties {
self.property_calls.fetch_add(1, Ordering::SeqCst);
self.properties.clone()
}
fn stat(&self, request: StatRequest<'_>) -> FsResult<StatResponse> {
self.stat_calls.fetch_add(1, Ordering::SeqCst);
if let Some(kind) = self.stat_error {
return Err(FsError::new(kind, FsOperation::Stat, "injected stat error"));
}
let path = if self.wrong_stat_path {
Path::parse("/wrong").expect("test path should parse")
} else {
request.path().clone()
};
Ok(StatResponse::new(path, FileMetadata::new(FileKind::File)))
}
fn list(&self, _: ListRequest<'_>) -> FsResult<OpenedDirectoryStream> {
Err(Self::unsupported())
}
fn open_reader(&self, _: OpenReaderRequest<'_>) -> FsResult<OpenedReader> {
Err(Self::unsupported())
}
fn open_writer(&self, _: OpenWriterRequest<'_>) -> FsResult<OpenedWriter> {
Err(Self::unsupported())
}
fn create_directory(&self, _: CreateDirectoryRequest<'_>) -> FsResult<CreateDirectoryOutcome> {
if self.direct_error {
Err(Self::unsupported())
} else {
Ok(CreateDirectoryOutcome::new(self.unexpected_create))
}
}
fn delete_file(&self, _: DeleteFileRequest<'_>) -> FsResult<DeleteOutcome> {
if self.direct_error {
Err(Self::unsupported())
} else {
Ok(DeleteOutcome::new(self.unexpected_delete))
}
}
fn delete_directory(&self, _: DeleteDirectoryRequest<'_>) -> FsResult<DeleteOutcome> {
if self.direct_error {
Err(Self::unsupported())
} else {
Ok(DeleteOutcome::new(self.unexpected_delete))
}
}
fn rename(&self, request: RenameRequest<'_>) -> Result<RenameOutcome, SpiRenameFailure> {
if self.direct_error {
Err(SpiRenameFailure::new(
Self::unsupported(),
RenameFailureState::Unchanged,
))
} else {
Ok(RenameOutcome::new(
request.source().clone(),
request.target().clone(),
AchievedAtomicity::Atomic,
PublicationMethod::AtomicRename,
))
}
}
fn create_temp_file(&self, _: CreateTempFileRequest) -> FsResult<OpenedTempFile> {
Err(Self::unsupported())
}
fn create_temp_directory(&self, _: CreateTempDirectoryRequest) -> FsResult<OpenedTempDirectory> {
Err(Self::unsupported())
}
}
struct DefaultSyncSpi {
properties: ProviderProperties,
}
impl FileSystemSpi for DefaultSyncSpi {
fn properties(&self) -> ProviderProperties {
self.properties.clone()
}
fn stat(&self, request: StatRequest<'_>) -> FsResult<StatResponse> {
Ok(StatResponse::new(
request.path().clone(),
FileMetadata::new(FileKind::File),
))
}
}
fn default_sync_properties() -> ProviderProperties {
ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("default-sync").expect("test provider id should be valid"),
"default-sync",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new()
.with_guaranteed(FileSystemCapability::List)
.with_guaranteed(FileSystemCapability::Read)
.with_guaranteed(FileSystemCapability::Write)
.with_guaranteed(FileSystemCapability::CreateDirectory)
.with_guaranteed(FileSystemCapability::Delete)
.with_guaranteed(FileSystemCapability::Rename)
.with_guaranteed(FileSystemCapability::Copy)
.with_guaranteed(FileSystemCapability::TempFile)
.with_guaranteed(FileSystemCapability::TempDirectory),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
)
.expect("default provider properties should be valid")
}
fn provider_operations() -> ProviderOperations {
ProviderOperations::new()
.with(ProviderOperation::Stat)
.with(ProviderOperation::List)
.with(ProviderOperation::OpenReader)
.with(ProviderOperation::OpenWriter)
.with(ProviderOperation::CreateDirectory)
.with(ProviderOperation::DeleteFile)
.with(ProviderOperation::DeleteDirectory)
.with(ProviderOperation::TryCopy)
.with(ProviderOperation::Rename)
.with(ProviderOperation::CreateTempFile)
.with(ProviderOperation::CreateTempDirectory)
}
#[test]
fn test_file_system_from_spi_caches_properties_snapshot() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("test").expect("test id should be valid"),
"test",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new(),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
);
let properties = properties.expect("properties should be valid");
let property_calls = Arc::new(AtomicUsize::new(0));
let stat_calls = Arc::new(AtomicUsize::new(0));
let filesystem = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::clone(&property_calls),
stat_calls: Arc::clone(&stat_calls),
wrong_stat_path: false,
stat_error: None,
direct_error: false,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
let clone = filesystem.clone();
assert_eq!(
filesystem.properties().info().provider_id(),
clone.properties().info().provider_id()
);
assert_eq!(1, property_calls.load(Ordering::SeqCst));
let relative = Path::parse("relative").expect("relative path should parse");
let error = filesystem
.stat(&relative)
.expect_err("absolute-only filesystem should reject relative path");
assert_eq!(FsErrorKind::InvalidPath, error.kind());
assert_eq!(0, stat_calls.load(Ordering::SeqCst));
}
#[test]
fn test_stat_rejects_path_with_different_semantics_before_spi_call() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("object-store").expect("test id should be valid"),
"test",
PathSemantics::ObjectKey,
),
provider_operations(),
FileSystemCapabilities::new(),
FileSystemLimits::unknown(),
PathConstraints::either(),
SymlinkPolicy::Reject,
)
.expect("properties should be valid");
let stat_calls = Arc::new(AtomicUsize::new(0));
let filesystem = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::clone(&stat_calls),
wrong_stat_path: false,
stat_error: None,
direct_error: false,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
let hierarchical = Path::parse("object").expect("hierarchical path should parse");
let error = filesystem
.stat(&hierarchical)
.expect_err("different path semantics must fail before SPI");
assert_eq!(FsErrorKind::InvalidPath, error.kind());
assert_eq!(FsOperation::Stat, error.operation());
assert_eq!(Some(&hierarchical), error.path());
assert_eq!(Some("test"), error.provider());
assert_eq!(0, stat_calls.load(Ordering::SeqCst));
}
#[test]
fn test_stat_rejects_provider_response_for_a_different_path() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("test").expect("test id should be valid"),
"test",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new(),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
)
.expect("properties should be valid");
let filesystem = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::new(AtomicUsize::new(0)),
wrong_stat_path: true,
stat_error: None,
direct_error: false,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
let path = Path::parse("/requested").expect("path should parse");
let error = filesystem
.stat(&path)
.expect_err("mismatched provider response must fail");
assert_eq!(FsErrorKind::ProviderContractViolation, error.kind());
}
#[test]
fn test_exists_maps_not_found_only_and_contextualizes_other_errors() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("test").expect("test id should be valid"),
"test",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new(),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
)
.expect("properties should be valid");
let path = Path::parse("/requested").expect("path should parse");
let missing = FileSystem::from_spi(CountingSpi {
properties: properties.clone(),
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::new(AtomicUsize::new(0)),
wrong_stat_path: false,
stat_error: Some(FsErrorKind::NotFound),
direct_error: false,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
assert!(!missing.exists(&path).expect("not found is a false result"));
let failed = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::new(AtomicUsize::new(0)),
wrong_stat_path: false,
stat_error: Some(FsErrorKind::Io),
direct_error: false,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
let error = failed.exists(&path).expect_err("non-not-found errors must propagate");
assert_eq!(FsErrorKind::Io, error.kind());
assert_eq!(FsOperation::Exists, error.operation());
}
#[test]
fn test_direct_sync_provider_failures_are_enriched() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("test").expect("test id should be valid"),
"test",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new()
.with_guaranteed(FileSystemCapability::CreateDirectory)
.with_guaranteed(FileSystemCapability::Delete)
.with_guaranteed(FileSystemCapability::Rename)
.with_guaranteed(FileSystemCapability::AtomicRename),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
)
.expect("properties should be valid");
let file_system = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::new(AtomicUsize::new(0)),
wrong_stat_path: false,
stat_error: None,
direct_error: true,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
let target = Path::parse("/target").expect("path should parse");
for error in [
file_system
.create_directory(&target, CreateDirectoryOptions::default())
.expect_err("provider create failure should propagate"),
file_system
.delete_file(&target, DeleteOptions::default())
.expect_err("provider delete failure should propagate"),
file_system
.delete_directory(&target, DeleteOptions::default())
.expect_err("provider delete failure should propagate"),
] {
assert_eq!(FsErrorKind::UnsupportedOperation, error.kind());
assert_eq!(Some(&target), error.path());
assert_eq!(Some("test"), error.provider());
}
let rename = file_system
.rename(
&Path::parse("/source").expect("path should parse"),
&target,
RenameOptions::default(),
)
.expect_err("provider rename failure should propagate");
assert_eq!(FsErrorKind::UnsupportedOperation, rename.error().kind());
assert_eq!(RenameFailureState::Unchanged, rename.state());
}
#[test]
fn test_sync_rename_returns_successful_provider_outcome() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("test").expect("test id should be valid"),
"test",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new()
.with_guaranteed(FileSystemCapability::Rename)
.with_guaranteed(FileSystemCapability::AtomicRename),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
)
.expect("properties should be valid");
let file_system = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::new(AtomicUsize::new(0)),
wrong_stat_path: false,
stat_error: None,
direct_error: false,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
let source = Path::parse("/source").expect("path should parse");
let target = Path::parse("/target").expect("path should parse");
let outcome = file_system
.rename(&source, &target, RenameOptions::default())
.expect("provider rename should succeed");
assert_eq!(&source, outcome.source());
assert_eq!(&target, outcome.target());
}
#[test]
fn test_sync_facade_rejects_unrequested_outcomes_and_same_path_mutations() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("test").expect("test id should be valid"),
"test",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new()
.with_guaranteed(FileSystemCapability::Copy)
.with_guaranteed(FileSystemCapability::CreateDirectory)
.with_guaranteed(FileSystemCapability::Delete)
.with_guaranteed(FileSystemCapability::Rename)
.with_guaranteed(FileSystemCapability::AtomicRename),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
)
.expect("properties should be valid");
let file_system = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::new(AtomicUsize::new(0)),
wrong_stat_path: false,
stat_error: None,
direct_error: false,
unexpected_create: true,
unexpected_delete: true,
})
.expect("facade should construct");
let path = Path::parse("/target").expect("path should parse");
for error in [
file_system
.create_directory(&path, CreateDirectoryOptions::default())
.expect_err("unexpected existing directory must be rejected"),
file_system
.delete_file(&path, DeleteOptions::default())
.expect_err("unexpected missing file must be rejected"),
file_system
.delete_directory(&path, DeleteOptions::default())
.expect_err("unexpected missing directory must be rejected"),
] {
assert_eq!(FsErrorKind::ProviderContractViolation, error.kind());
}
let copy = file_system
.copy(&path, &path, CopyOptions::default())
.expect_err("copy source and target must differ");
assert_eq!(FsErrorKind::InvalidOptions, copy.error().kind());
let rename = file_system
.rename(&path, &path, RenameOptions::default())
.expect_err("rename source and target must differ");
assert_eq!(FsErrorKind::InvalidOptions, rename.error().kind());
assert_eq!(Some("test"), rename.error().provider());
}
#[test]
fn test_sync_facade_requires_operation_capabilities_before_dispatch() {
let properties = ProviderProperties::new(
FileSystemInfo::new(
FileSystemId::new("test").expect("test id should be valid"),
"test",
PathSemantics::Hierarchical,
),
provider_operations(),
FileSystemCapabilities::new(),
FileSystemLimits::unknown(),
PathConstraints::absolute(),
SymlinkPolicy::Reject,
)
.expect("properties should be valid");
let file_system = FileSystem::from_spi(CountingSpi {
properties,
property_calls: Arc::new(AtomicUsize::new(0)),
stat_calls: Arc::new(AtomicUsize::new(0)),
wrong_stat_path: false,
stat_error: None,
direct_error: false,
unexpected_create: false,
unexpected_delete: false,
})
.expect("facade should construct");
let path = Path::parse("/target").expect("path should parse");
let reader = file_system
.open_reader(&path, ReadOptions::default())
.expect_err("capability");
let writer = file_system
.open_writer(&path, WriteOptions::default())
.expect_err("capability");
let file = file_system
.create_temp_file(TempOptions::default())
.expect_err("capability");
let directory = file_system
.create_temp_directory(TempOptions::default())
.expect_err("capability");
let create = file_system
.create_directory(&path, CreateDirectoryOptions::default())
.expect_err("capability");
for error in [&reader, writer.error(), file.error(), directory.error(), &create] {
assert_eq!(FsErrorKind::UnsupportedCapability, error.kind());
}
assert!(writer.recovery().is_none());
assert!(file.recovery().is_none());
assert!(directory.recovery().is_none());
let rename = file_system
.rename(&path, &Path::root(), RenameOptions::default())
.expect_err("rename requires advertised capability");
assert_eq!(FsErrorKind::UnsupportedCapability, rename.error().kind());
}
#[test]
fn test_sync_spi_default_operations_report_unsupported() {
let filesystem = FileSystem::from_spi(DefaultSyncSpi {
properties: default_sync_properties(),
})
.expect("default provider facade should construct");
let path = Path::parse("/resource").expect("test path should parse");
let target = Path::parse("/target").expect("test target should parse");
let error = match filesystem.list(&ListScope::Path((path).clone()), ListOptions::default()) {
Ok(_) => panic!("default list implementation must reject the request"),
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.kind());
assert_eq!(FsOperation::List, error.operation());
let error = match filesystem.open_reader(&path, ReadOptions::default()) {
Ok(_) => {
panic!("default reader implementation must reject the request")
}
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.kind());
assert_eq!(FsOperation::OpenReader, error.operation());
let error = match filesystem.open_writer(&path, WriteOptions::default()) {
Ok(_) => {
panic!("default writer implementation must reject the request")
}
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.error().kind());
assert_eq!(FsOperation::OpenWriter, error.error().operation());
let error = match filesystem.create_directory(&path, CreateDirectoryOptions::default()) {
Ok(_) => {
panic!("default directory implementation must reject the request")
}
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.kind());
assert_eq!(FsOperation::CreateDir, error.operation());
let error = match filesystem.delete_file(&path, DeleteOptions::default()) {
Ok(_) => panic!("default file deletion must reject the request"),
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.kind());
assert_eq!(FsOperation::Delete, error.operation());
let error = match filesystem.delete_directory(&path, DeleteOptions::default()) {
Ok(_) => panic!("default directory deletion must reject the request"),
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.kind());
assert_eq!(FsOperation::Delete, error.operation());
let error = match filesystem.rename(&path, &target, RenameOptions::default()) {
Ok(_) => {
panic!("default rename implementation must reject the request")
}
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.error().kind());
assert_eq!(FsOperation::Rename, error.error().operation());
let error = match filesystem.copy(&path, &target, CopyOptions::default()) {
Ok(_) => panic!("default copy implementation must not complete"),
Err(error) => error,
};
assert_eq!(FsOperation::Copy, error.error().operation());
let error = match filesystem.create_temp_file(TempOptions::default()) {
Ok(_) => panic!("default temporary-file implementation must reject the request"),
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.error().kind());
assert_eq!(FsOperation::CreateTemp, error.error().operation());
let error = match filesystem.create_temp_directory(TempOptions::default()) {
Ok(_) => panic!("default temporary-directory implementation must reject the request"),
Err(error) => error,
};
assert_eq!(FsErrorKind::UnsupportedOperation, error.error().kind());
assert_eq!(FsOperation::CreateTemp, error.error().operation());
}