You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/06/11 23:33:43 UTC

(opendal) 01/01: Add suffix read behavior tests

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/suffix-read-behavior-tests
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit cfd88e32cbafb6e34302b9d9762649aeba9e0e29
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Fri Jun 12 07:33:29 2026 +0800

    Add suffix read behavior tests
---
 core/tests/behavior/async_read.rs | 118 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 118 insertions(+)

diff --git a/core/tests/behavior/async_read.rs b/core/tests/behavior/async_read.rs
index 89fedf164..049f95904 100644
--- a/core/tests/behavior/async_read.rs
+++ b/core/tests/behavior/async_read.rs
@@ -58,6 +58,16 @@ pub fn tests(op: &Operator, tests: &mut Vec<Trial>) {
         ))
     }
 
+    if cap.read && cap.write && cap.read_with_suffix {
+        tests.extend(async_trials!(
+            op,
+            test_read_suffix,
+            test_read_suffix_larger_than_file,
+            test_read_suffix_zero,
+            test_reader_suffix_with_chunk
+        ))
+    }
+
     if cap.read && !cap.write {
         tests.extend(async_trials!(
             op,
@@ -72,6 +82,10 @@ pub fn tests(op: &Operator, tests: &mut Vec<Trial>) {
             test_reader_only_read_with_if_none_match
         ))
     }
+
+    if cap.read && !cap.write && cap.read_with_suffix {
+        tests.extend(async_trials!(op, test_read_only_read_with_suffix))
+    }
 }
 
 /// Read full content should match.
@@ -113,6 +127,93 @@ pub async fn test_read_range(op: Operator) -> anyhow::Result<()> {
     Ok(())
 }
 
+/// Read suffix content should match.
+pub async fn test_read_suffix(op: Operator) -> anyhow::Result<()> {
+    let path = TEST_FIXTURE.new_file_path();
+    let content = gen_fixed_bytes(1024);
+    let suffix_size = 257;
+
+    op.write(&path, content.clone())
+        .await
+        .expect("write must succeed");
+
+    let bs = op
+        .read_with(&path)
+        .range(BytesRange::suffix(suffix_size as u64))
+        .await?
+        .to_bytes();
+    assert_eq!(bs.len(), suffix_size, "read size");
+    assert_eq!(
+        sha256_digest(&bs),
+        sha256_digest(&content[content.len() - suffix_size..]),
+        "read content"
+    );
+
+    Ok(())
+}
+
+/// Read suffix larger than the file should return the full content.
+pub async fn test_read_suffix_larger_than_file(op: Operator) -> anyhow::Result<()> {
+    let path = TEST_FIXTURE.new_file_path();
+    let content = gen_fixed_bytes(1024);
+
+    op.write(&path, content.clone())
+        .await
+        .expect("write must succeed");
+
+    let bs = op
+        .read_with(&path)
+        .range(BytesRange::suffix((content.len() * 2) as u64))
+        .await?
+        .to_bytes();
+    assert_eq!(bs.len(), content.len(), "read size");
+    assert_eq!(sha256_digest(&bs), sha256_digest(&content), "read content");
+
+    Ok(())
+}
+
+/// Read zero suffix should return empty content.
+pub async fn test_read_suffix_zero(op: Operator) -> anyhow::Result<()> {
+    let path = TEST_FIXTURE.new_file_path();
+    let content = gen_fixed_bytes(1024);
+
+    op.write(&path, content).await.expect("write must succeed");
+
+    let bs = op
+        .read_with(&path)
+        .range(BytesRange::suffix(0))
+        .await?
+        .to_bytes();
+    assert!(bs.is_empty(), "read content must be empty");
+
+    Ok(())
+}
+
+/// Reader suffix with chunk should still return the requested suffix content.
+pub async fn test_reader_suffix_with_chunk(op: Operator) -> anyhow::Result<()> {
+    let path = TEST_FIXTURE.new_file_path();
+    let content = gen_fixed_bytes(4096);
+    let suffix_size = 1025;
+
+    op.write(&path, content.clone())
+        .await
+        .expect("write must succeed");
+
+    let reader = op.reader_with(&path).chunk(257).concurrent(3).await?;
+    let bs = reader
+        .read(BytesRange::suffix(suffix_size as u64))
+        .await?
+        .to_bytes();
+    assert_eq!(bs.len(), suffix_size, "read size");
+    assert_eq!(
+        sha256_digest(&bs),
+        sha256_digest(&content[content.len() - suffix_size..]),
+        "read content"
+    );
+
+    Ok(())
+}
+
 /// Read full content should match.
 pub async fn test_reader(op: Operator) -> anyhow::Result<()> {
     let (path, content, size) = TEST_FIXTURE.new_file(op.clone());
@@ -800,6 +901,23 @@ pub async fn test_read_only_read_with_range(op: Operator) -> anyhow::Result<()>
     Ok(())
 }
 
+/// Read suffix content should match.
+pub async fn test_read_only_read_with_suffix(op: Operator) -> anyhow::Result<()> {
+    let bs = op
+        .read_with("normal_file.txt")
+        .range(BytesRange::suffix(1024))
+        .await?
+        .to_bytes();
+    assert_eq!(bs.len(), 1024, "read size");
+    assert_eq!(
+        sha256_digest(&bs),
+        "cc9312c869238ea9410b6716e0fc3f48056f2bfb2fe06ccf5f96f2c3bf39e71b",
+        "read content"
+    );
+
+    Ok(())
+}
+
 /// Read not exist file should return NotFound
 pub async fn test_read_only_read_not_exist(op: Operator) -> anyhow::Result<()> {
     let path = uuid::Uuid::new_v4().to_string();