This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 958f5453 test(core): test for append (#2286)
958f5453 is described below
commit 958f5453637b46f587675696c12848f142c55ac4
Author: Suyan <[email protected]>
AuthorDate: Mon May 22 23:05:45 2023 +0800
test(core): test for append (#2286)
* test(core): test for append
Signed-off-by: suyanhanx <[email protected]>
* merge append_to_non_exsiting_target to append
Signed-off-by: suyanhanx <[email protected]>
---------
Signed-off-by: suyanhanx <[email protected]>
---
core/src/types/capability.rs | 6 +
core/src/types/operator/metadata.rs | 5 +
core/tests/behavior/append.rs | 223 ++++++++++++++++++++++++++++++++++++
core/tests/behavior/main.rs | 4 +
core/tests/behavior/write.rs | 30 +++++
5 files changed, 268 insertions(+)
diff --git a/core/src/types/capability.rs b/core/src/types/capability.rs
index 14685c30..c6b8e981 100644
--- a/core/src/types/capability.rs
+++ b/core/src/types/capability.rs
@@ -88,6 +88,12 @@ pub struct Capability {
/// If operator supports append natively, it will be true.
pub append: bool,
+ /// If operator supports append with content type natively, it will be
true.
+ pub append_with_content_type: bool,
+ /// If operator supports append with content disposition natively, it will
be true.
+ pub append_with_content_disposition: bool,
+ /// If operator supports append with cache control natively, it will be
true.
+ pub append_with_cache_control: bool,
/// If operator supports create dir natively, it will be true.
pub create_dir: bool,
diff --git a/core/src/types/operator/metadata.rs
b/core/src/types/operator/metadata.rs
index d4d850f7..3d219dfc 100644
--- a/core/src/types/operator/metadata.rs
+++ b/core/src/types/operator/metadata.rs
@@ -62,6 +62,11 @@ impl OperatorInfo {
self.0.capability().write
}
+ /// Check if current backend supports [`Accessor::append`] or not.
+ pub fn can_append(&self) -> bool {
+ self.0.capability().append
+ }
+
/// Check if current backend supports [`Accessor::copy`] or not.
pub fn can_copy(&self) -> bool {
self.0.capability().copy
diff --git a/core/tests/behavior/append.rs b/core/tests/behavior/append.rs
new file mode 100644
index 00000000..1c03a3e6
--- /dev/null
+++ b/core/tests/behavior/append.rs
@@ -0,0 +1,223 @@
+// 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 opendal::ops::OpAppend;
+use opendal::EntryMode;
+use opendal::ErrorKind;
+use opendal::Operator;
+
+use super::utils::*;
+
+/// Test services that meet the following capability:
+///
+/// - can_read
+/// - can_write
+/// - can_append
+macro_rules! behavior_append_test {
+ ($service:ident, $($(#[$meta:meta])* $test:ident),*,) => {
+ paste::item! {
+ $(
+ #[test]
+ $(
+ #[$meta]
+ )*
+ fn [<append_ $test >]() -> anyhow::Result<()> {
+ match OPERATOR.as_ref() {
+ Some(op) if op.info().can_read()
+ && op.info().can_write()
+ && op.info().can_append() =>
RUNTIME.block_on($crate::append::$test(op.clone())),
+ Some(_) => {
+ log::warn!("service {} doesn't support append,
ignored", opendal::Scheme::$service);
+ Ok(())
+ },
+ None => {
+ Ok(())
+ }
+ }
+ }
+ )*
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! behavior_append_tests {
+ ($($service:ident),*) => {
+ $(
+ behavior_append_test!(
+ $service,
+
+ test_append,
+ test_append_with_dir_path,
+ test_append_with_cache_control,
+ test_append_with_content_type,
+ test_append_with_content_disposition,
+
+ test_fuzz_appender,
+ );
+ )*
+ };
+}
+
+/// Test append to a file must success.
+pub async fn test_append(op: Operator) -> Result<()> {
+ let path = uuid::Uuid::new_v4().to_string();
+ let (content_one, size_one) = gen_bytes();
+ let (content_two, size_two) = gen_bytes();
+
+ op.append(&path, content_one.clone())
+ .await
+ .expect("append file first time must success");
+
+ op.append(&path, content_two.clone())
+ .await
+ .expect("append to an existing file must success");
+
+ let bs = op.read(&path).await.expect("read file must success");
+
+ assert_eq!(bs.len(), size_one + size_two);
+ assert_eq!(bs[..size_one], content_one);
+ assert_eq!(bs[size_one..], content_two);
+
+ op.delete(&path).await.expect("delete file must success");
+
+ Ok(())
+}
+
+/// Test append to a directory path must fail.
+pub async fn test_append_with_dir_path(op: Operator) -> Result<()> {
+ let path = uuid::Uuid::new_v4().to_string();
+ let (content, _) = gen_bytes();
+
+ let res = op.append(&path, content).await;
+ assert!(res.is_err());
+ assert_eq!(res.unwrap_err().kind(), ErrorKind::IsADirectory);
+
+ Ok(())
+}
+
+/// Test append with cache control must success.
+pub async fn test_append_with_cache_control(op: Operator) -> Result<()> {
+ if !op.info().capability().write_with_cache_control {
+ return Ok(());
+ }
+
+ let path = uuid::Uuid::new_v4().to_string();
+ let (content, _) = gen_bytes();
+
+ let target_cache_control = "no-cache, no-store, max-age=300";
+
+ let mut op_append = OpAppend::default();
+ op_append = op_append.with_cache_control(target_cache_control);
+
+ op.append_with(&path, op_append, content).await?;
+
+ let meta = op.stat(&path).await.expect("stat must succeed");
+ assert_eq!(meta.mode(), EntryMode::FILE);
+ assert_eq!(
+ meta.cache_control().expect("cache control must exist"),
+ target_cache_control
+ );
+
+ op.delete(&path).await.expect("delete must succeed");
+
+ Ok(())
+}
+
+/// Test append with content type must success.
+pub async fn test_append_with_content_type(op: Operator) -> Result<()> {
+ if !op.info().capability().append_with_content_type {
+ return Ok(());
+ }
+
+ let path = uuid::Uuid::new_v4().to_string();
+ let (content, size) = gen_bytes();
+
+ let target_content_type = "application/json";
+
+ let mut op_append = OpAppend::default();
+ op_append = op_append.with_content_type(target_content_type);
+
+ op.append_with(&path, op_append, content).await?;
+
+ let meta = op.stat(&path).await.expect("stat must succeed");
+ assert_eq!(meta.mode(), EntryMode::FILE);
+ assert_eq!(
+ meta.content_type().expect("content type must exist"),
+ target_content_type
+ );
+ assert_eq!(meta.content_length(), size as u64);
+
+ op.delete(&path).await.expect("delete must succeed");
+
+ Ok(())
+}
+
+/// Write a single file with content disposition should succeed.
+pub async fn test_append_with_content_disposition(op: Operator) -> Result<()> {
+ if !op.info().capability().write_with_content_disposition {
+ return Ok(());
+ }
+
+ let path = uuid::Uuid::new_v4().to_string();
+ let (content, size) = gen_bytes();
+
+ let target_content_disposition = "attachment; filename=\"filename.jpg\"";
+
+ let mut op_append = OpAppend::default();
+ op_append = op_append.with_content_disposition(target_content_disposition);
+
+ op.append_with(&path, op_append, content).await?;
+
+ let meta = op.stat(&path).await.expect("stat must succeed");
+ assert_eq!(meta.mode(), EntryMode::FILE);
+ assert_eq!(
+ meta.content_disposition().expect("content type must exist"),
+ target_content_disposition
+ );
+ assert_eq!(meta.content_length(), size as u64);
+
+ op.delete(&path).await.expect("delete must succeed");
+
+ Ok(())
+}
+
+/// Test for fuzzing appender.
+pub async fn test_fuzz_appender(op: Operator) -> Result<()> {
+ let path = uuid::Uuid::new_v4().to_string();
+
+ let mut fuzzer = ObjectWriterFuzzer::new(&path, None);
+
+ let mut a = op.appender(&path).await?;
+
+ for _ in 0..100 {
+ match fuzzer.fuzz() {
+ ObjectWriterAction::Write(bs) => {
+ a.append(bs).await?;
+ }
+ }
+ }
+ a.close().await?;
+
+ let content = op.read(&path).await?;
+ fuzzer.check(&content);
+
+ op.delete(&path).await.expect("delete file must success");
+
+ Ok(())
+}
diff --git a/core/tests/behavior/main.rs b/core/tests/behavior/main.rs
index 3a80f0cd..54fae9e4 100644
--- a/core/tests/behavior/main.rs
+++ b/core/tests/behavior/main.rs
@@ -26,6 +26,8 @@ mod blocking_read;
#[macro_use]
mod blocking_write;
#[macro_use]
+mod append;
+#[macro_use]
mod copy;
#[macro_use]
mod list;
@@ -69,6 +71,8 @@ macro_rules! behavior_tests {
behavior_write_tests!($service);
// can_read && can_write && can_blocking
behavior_blocking_write_tests!($service);
+ // can_read && can_write && can_append
+ behavior_append_tests!($service);
// can_read && can_write && can_copy
behavior_copy_tests!($service);
// can read && can_write && can_blocking && can_copy
diff --git a/core/tests/behavior/write.rs b/core/tests/behavior/write.rs
index fa5b47b2..0da8a1d2 100644
--- a/core/tests/behavior/write.rs
+++ b/core/tests/behavior/write.rs
@@ -80,6 +80,7 @@ macro_rules! behavior_write_tests {
test_write_with_special_chars,
test_write_with_cache_control,
test_write_with_content_type,
+ test_write_with_content_disposition,
test_stat,
test_stat_dir,
test_stat_with_special_chars,
@@ -249,6 +250,35 @@ pub async fn test_write_with_content_type(op: Operator) ->
Result<()> {
Ok(())
}
+/// Write a single file with content disposition should succeed.
+pub async fn test_write_with_content_disposition(op: Operator) -> Result<()> {
+ if !op.info().capability().write_with_content_disposition {
+ return Ok(());
+ }
+
+ let path = uuid::Uuid::new_v4().to_string();
+ let (content, size) = gen_bytes();
+
+ let target_content_disposition = "attachment; filename=\"filename.jpg\"";
+
+ let mut op_write = OpWrite::default();
+ op_write = op_write.with_content_disposition(target_content_disposition);
+
+ op.write_with(&path, op_write, content).await?;
+
+ let meta = op.stat(&path).await.expect("stat must succeed");
+ assert_eq!(meta.mode(), EntryMode::FILE);
+ assert_eq!(
+ meta.content_disposition().expect("content type must exist"),
+ target_content_disposition
+ );
+ assert_eq!(meta.content_length(), size as u64);
+
+ op.delete(&path).await.expect("delete must succeed");
+
+ Ok(())
+}
+
/// Stat existing file should return metadata
pub async fn test_stat(op: Operator) -> Result<()> {
let path = uuid::Uuid::new_v4().to_string();