blob: ae441cc54970e41867c56abcd4cb6b3c111135f0 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use anyhow::Result;
use futures::TryStreamExt;
use opendal::layers::CapabilityOverrideLayer;
use opendal::raw::OpDelete;
use crate::*;
pub fn tests(op: &Operator, tests: &mut Vec<Trial>) {
let cap = op.info().capability();
if cap.stat && cap.delete && cap.write {
tests.extend(async_trials!(
op,
test_delete_file,
test_delete_empty_dir,
test_delete_with_special_chars,
test_delete_not_existing,
test_delete_stream,
test_remove_one_file,
test_delete_with_version,
test_delete_with_not_existing_version,
test_batch_delete,
test_batch_delete_with_version
));
if cap.delete_with_recursive {
tests.extend(async_trials!(op, test_delete_with_recursive_basic));
}
if cap.list_with_recursive {
tests.extend(async_trials!(op, test_remove_all_basic));
if !cap.create_dir {
tests.extend(async_trials!(op, test_remove_all_with_prefix_exists));
}
}
}
}
/// Delete existing file should succeed.
pub async fn test_delete_file(op: Operator) -> Result<()> {
let (path, content, _) = TEST_FIXTURE.new_file(op.clone());
op.write(&path, content).await.expect("write must succeed");
op.delete(&path).await?;
// Stat it again to check.
assert!(!op.exists(&path).await?);
Ok(())
}
/// Delete empty dir should succeed.
pub async fn test_delete_empty_dir(op: Operator) -> Result<()> {
if !op.info().capability().create_dir {
return Ok(());
}
let path = TEST_FIXTURE.new_dir_path();
op.create_dir(&path).await.expect("create must succeed");
op.delete(&path).await?;
Ok(())
}
/// Delete file with special chars should succeed.
pub async fn test_delete_with_special_chars(op: Operator) -> Result<()> {
let path = format!("{} !@#$%^&()_+-=;',.txt", uuid::Uuid::new_v4());
let (path, content, _) = TEST_FIXTURE.new_file_with_path(op.clone(), &path);
op.write(&path, content).await.expect("write must succeed");
op.delete(&path).await?;
// Stat it again to check.
assert!(!op.exists(&path).await?);
Ok(())
}
/// Delete not existing file should also succeed.
pub async fn test_delete_not_existing(op: Operator) -> Result<()> {
let path = uuid::Uuid::new_v4().to_string();
op.delete(&path).await?;
Ok(())
}
/// Remove one file
pub async fn test_remove_one_file(op: Operator) -> Result<()> {
let (path, content, _) = TEST_FIXTURE.new_file(op.clone());
op.write(&path, content.clone())
.await
.expect("write must succeed");
op.delete_iter(vec![path.clone()]).await?;
// Stat it again to check.
assert!(!op.exists(&path).await?);
op.write(&format!("/{path}"), content)
.await
.expect("write must succeed");
op.delete_iter(vec![path.clone()]).await?;
// Stat it again to check.
assert!(!op.exists(&path).await?);
Ok(())
}
/// Delete via stream.
pub async fn test_delete_stream(op: Operator) -> Result<()> {
if !op.info().capability().create_dir {
return Ok(());
}
// Gdrive think that this test is an abuse of their service and redirect us
// to an infinite loop. Let's ignore this test for gdrive.
#[cfg(feature = "services-gdrive")]
if op.info().scheme() == services::GDRIVE_SCHEME {
return Ok(());
}
let dir = uuid::Uuid::new_v4().to_string();
op.create_dir(&format!("{dir}/"))
.await
.expect("create must succeed");
let expected: Vec<_> = (0..100).collect();
for path in expected.iter() {
op.write(&format!("{dir}/{path}"), "delete_stream").await?;
}
let mut deleter = op.deleter().await?;
deleter
.delete_iter(expected.iter().map(|v| format!("{dir}/{v}")))
.await?;
deleter.close().await?;
// Stat it again to check.
for path in expected.iter() {
assert!(
!op.exists(&format!("{dir}/{path}")).await?,
"{path} should be removed"
)
}
Ok(())
}
async fn test_blocking_remove_all_with_objects(
op: Operator,
parent: String,
paths: impl IntoIterator<Item = &'static str>,
) -> Result<()> {
for path in paths {
let path = format!("{parent}/{path}");
let (content, _) = gen_bytes(op.info().capability());
op.write(&path, content).await.expect("write must succeed");
}
op.delete_with(&parent).recursive(true).await?;
let found = op
.lister_with(&format!("{parent}/"))
.recursive(true)
.await
.expect("list must succeed")
.try_next()
.await
.expect("list must succeed")
.is_some();
assert!(!found, "all objects should be removed");
Ok(())
}
/// Remove all under a prefix
pub async fn test_remove_all_basic(op: Operator) -> Result<()> {
#[cfg(feature = "services-hf")]
{
if op.info().scheme() == services::HF_SCHEME {
// Hugging Face only guarantees recursive listing for repository trees,
// while this case expects prefix-recursive semantics for a non-directory path.
return Ok(());
}
}
let parent = uuid::Uuid::new_v4().to_string();
test_blocking_remove_all_with_objects(op, parent, ["a/b", "a/c", "a/d/e"]).await
}
/// Remove all under a prefix, while the prefix itself is also an object.
///
/// This test requires flat key storage where a path can be both a file
/// and a directory prefix simultaneously (e.g., S3). Services with real
/// directory semantics (e.g., git-based repos) cannot support this
/// because a path cannot be both a file and a directory.
pub async fn test_remove_all_with_prefix_exists(op: Operator) -> Result<()> {
#[cfg(feature = "services-hf")]
{
if op.info().scheme() == services::HF_SCHEME {
// Hugging Face does not provide a stable recursive delete contract
// for repository trees under concurrent commits.
return Ok(());
}
}
let parent = uuid::Uuid::new_v4().to_string();
let (content, _) = gen_bytes(op.info().capability());
op.write(&parent, content)
.await
.expect("write must succeed");
test_blocking_remove_all_with_objects(op, parent, ["a", "a/b", "a/c", "a/b/e"]).await
}
pub async fn test_delete_with_version(op: Operator) -> Result<()> {
if !op.info().capability().delete_with_version {
return Ok(());
}
let (path, content, _) = TEST_FIXTURE.new_file(op.clone());
op.write(path.as_str(), content)
.await
.expect("write must success");
let meta = op.stat(path.as_str()).await.expect("stat must success");
let version = meta.version().expect("must have version");
op.delete(path.as_str()).await.expect("delete must success");
assert!(!op.exists(path.as_str()).await?);
// After a simple delete, the data can still be accessed using its version.
let meta = op
.stat_with(path.as_str())
.version(version)
.await
.expect("stat must success");
assert_eq!(version, meta.version().expect("must have version"));
// After deleting with the version, the data is removed permanently
op.delete_with(path.as_str())
.version(version)
.await
.expect("delete must success");
let ret = op.stat_with(path.as_str()).version(version).await;
assert!(ret.is_err());
assert_eq!(ret.unwrap_err().kind(), ErrorKind::NotFound);
Ok(())
}
pub async fn test_delete_with_not_existing_version(op: Operator) -> Result<()> {
if !op.info().capability().delete_with_version {
return Ok(());
}
// retrieve a valid version
let (path, content, _) = TEST_FIXTURE.new_file(op.clone());
op.write(path.as_str(), content)
.await
.expect("write must success");
let version = op
.stat(path.as_str())
.await
.expect("stat must success")
.version()
.expect("must have stat")
.to_string();
let (path, content, _) = TEST_FIXTURE.new_file(op.clone());
op.write(path.as_str(), content)
.await
.expect("write must success");
let ret = op
.delete_with(path.as_str())
.version(version.as_str())
.await;
assert!(ret.is_ok());
Ok(())
}
pub async fn test_delete_with_recursive_basic(op: Operator) -> Result<()> {
if !op.info().capability().delete_with_recursive {
return Ok(());
}
#[cfg(feature = "services-hf")]
{
if op.info().scheme() == services::HF_SCHEME {
// Hugging Face does not provide a stable recursive delete contract
// for repository trees under concurrent commits.
return Ok(());
}
}
let base = format!("delete_recursive_{}/", uuid::Uuid::new_v4());
let files = [
format!("{base}file.txt"),
format!("{base}dir1/file1.txt"),
format!("{base}dir1/dir2/file2.txt"),
];
for path in &files {
op.write(path, "delete recursive").await?;
}
op.delete_with(&base).recursive(true).await?;
let mut l = op.lister_with(&base).recursive(true).await?;
assert!(
l.try_next().await?.is_none(),
"all entries should be removed"
);
for path in &files {
assert!(!op.exists(path).await?, "{path} should be removed");
}
Ok(())
}
pub async fn test_batch_delete(op: Operator) -> Result<()> {
let mut cap = op.info().capability();
if cap.delete_max_size.unwrap_or(1) <= 1 {
return Ok(());
}
cap.delete_max_size = Some(2);
let op = op.layer(CapabilityOverrideLayer::new(move |_| cap));
let mut files = Vec::new();
for _ in 0..5 {
let (path, content, _) = TEST_FIXTURE.new_file(op.clone());
op.write(path.as_str(), content)
.await
.expect("write must succeed");
files.push(path);
}
op.delete_iter(files.clone())
.await
.expect("batch delete must succeed");
for path in files {
let stat = op.stat(path.as_str()).await;
assert!(stat.is_err());
assert_eq!(stat.unwrap_err().kind(), ErrorKind::NotFound);
}
Ok(())
}
pub async fn test_batch_delete_with_version(op: Operator) -> Result<()> {
let mut cap = op.info().capability();
if !cap.delete_with_version {
return Ok(());
}
if cap.delete_max_size.unwrap_or(1) <= 1 {
return Ok(());
}
cap.delete_max_size = Some(2);
let op = op.layer(CapabilityOverrideLayer::new(move |_| cap));
let mut files = Vec::new();
for _ in 0..5 {
let (path, content, _) = TEST_FIXTURE.new_file(op.clone());
op.write(path.as_str(), content)
.await
.expect("write must succeed");
let meta = op.stat(path.as_str()).await.expect("stat must succeed");
let version = meta.version().expect("must have version");
let op_args = OpDelete::new().with_version(version);
files.push((path, op_args));
}
op.delete_iter(files.clone())
.await
.expect("batch delete must succeed");
for (path, args) in files {
let stat = op
.stat_with(path.as_str())
.version(args.version().unwrap())
.await;
assert!(stat.is_err());
assert_eq!(stat.unwrap_err().kind(), ErrorKind::NotFound);
}
Ok(())
}