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 2024/01/07 03:51:52 UTC

(incubator-opendal) 01/01: ci: Refactor fuzz to integrate with test planner

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

xuanwo pushed a commit to branch refactor-fuzz
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git

commit c71fb8f72346571fe7ff386f3efc44f8d1233f39
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Sun Jan 7 11:51:29 2024 +0800

    ci: Refactor fuzz to integrate with test planner
    
    Signed-off-by: Xuanwo <gi...@xuanwo.io>
---
 .github/actions/fuzz_test/action.yaml |  49 ++++++++++
 .github/workflows/fuzz_test.yml       | 176 ++++------------------------------
 Cargo.lock                            |   1 +
 core/fuzz/Cargo.toml                  |   5 +-
 core/fuzz/fuzz_reader.rs              |  25 ++++-
 core/fuzz/fuzz_reader_with_buffer.rs  | 164 -------------------------------
 core/fuzz/fuzz_writer.rs              |   6 ++
 7 files changed, 98 insertions(+), 328 deletions(-)

diff --git a/.github/actions/fuzz_test/action.yaml b/.github/actions/fuzz_test/action.yaml
new file mode 100644
index 0000000000..d23dd12b15
--- /dev/null
+++ b/.github/actions/fuzz_test/action.yaml
@@ -0,0 +1,49 @@
+# 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.
+
+name: Fuzz Test
+description: 'Fuzz test given setup and service'
+inputs:
+  setup:
+    description: "The setup action for test"
+  service:
+    description: "The service to test"
+  target:
+    description: "The fuzz target to test"
+
+runs:
+  using: "composite"
+  steps:
+    - name: Setup
+      shell: bash
+      run: |
+        mkdir -p ./dynamic_fuzz_test &&
+        cat <<EOF >./dynamic_fuzz_test/action.yml
+        runs:
+          using: composite
+          steps:
+          - name: Setup Fuzz Test
+            uses: ./.github/services/${{ inputs.service }}/${{ inputs.setup }}
+          - name: Run Fuzz Test
+            shell: bash
+            working-directory: core/fuzz
+            run: cargo +nightly fuzz run ${{ inputs.target }} --features tests,services-${{ inputs.service }} -- -max_total_time=120
+            env:
+              OPENDAL_TEST: ${{ inputs.service }}
+        EOF
+    - name: Run
+      uses: ./dynamic_fuzz_test
diff --git a/.github/workflows/fuzz_test.yml b/.github/workflows/fuzz_test.yml
index 97652f040e..a67e783fbf 100644
--- a/.github/workflows/fuzz_test.yml
+++ b/.github/workflows/fuzz_test.yml
@@ -28,10 +28,6 @@ on:
       - "core/src/**"
       - "core/fuzz/**"
       - "!core/src/docs/**"
-      - "!core/src/services/**"
-      - "core/src/services/fs/**"
-      - "core/src/services/memory/**"
-      - "core/src/services/s3/**"
       - ".github/workflows/fuzz_test.yml"
 
 concurrency:
@@ -39,164 +35,32 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  fuzz-build:
+  fuzz:
+    name: ${{ matrix.cases.service }} / ${{ matrix.cases.setup }} / ${{ matrix.target }}
     runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        target: [ "fuzz_reader", "fuzz_writer" ]
+        cases:
+          - { service: "memory", setup: "memory" }
+          - { service: "fs", setup: "local_fs" }
+          - { service: "s3", setup: "minio_s3" }
+          - { service: "azblob", setup: "azurite_azblob" }
     steps:
       - uses: actions/checkout@v4
       - name: Setup Rust toolchain
         uses: ./.github/actions/setup
-      - name: Install libfuzz
-        shell: bash
-        run: sudo apt-get install -y libfuzzer-14-dev
-
-      - name: Install cargo fuzz
+      - name: Set Rust Fuzz
         shell: bash
-        run: rustup install nightly && cargo +nightly install cargo-fuzz
-      - name: Build Fuzz Targets
-        shell: bash
-        working-directory: core/fuzz
-        run: cargo +nightly fuzz build
+        run: |
+          sudo apt-get install -y libfuzzer-14-dev
+          rustup install nightly
+          cargo +nightly install cargo-fuzz
+      - name: Fuzz
+        uses: ./.github/actions/fuzz_test
         env:
           CUSTOM_LIBFUZZER_PATH: /usr/lib/llvm-14/lib/libFuzzer.a
-
-      - name: Upload build artifacts
-        uses: actions/upload-artifact@v3
         with:
-          name: fuzz_targets
-          path: |
-            ./target/x86_64-unknown-linux-gnu/release/fuzz_reader
-            ./target/x86_64-unknown-linux-gnu/release/fuzz_reader_with_buffer
-            ./target/x86_64-unknown-linux-gnu/release/fuzz_writer
-
-  fuzz-test-s3:
-    runs-on: ubuntu-latest
-    needs: fuzz-build
-    services:
-      minio:
-        image: wktk/minio-server
-        ports:
-          - 9000:9000
-        env:
-          MINIO_ACCESS_KEY: "minioadmin"
-          MINIO_SECRET_KEY: "minioadmin"
-    strategy:
-      fail-fast: true
-      matrix:
-        fuzz-targets: [fuzz_reader, fuzz_reader_with_buffer, fuzz_writer]
-    steps:
-      - name: Install libfuzzer
-        run: sudo apt-get install -y libfuzzer-14-dev
-      - name: Setup Test Bucket
-        env:
-          AWS_ACCESS_KEY_ID: "minioadmin"
-          AWS_SECRET_ACCESS_KEY: "minioadmin"
-          AWS_EC2_METADATA_DISABLED: "true"
-        run: aws --endpoint-url http://127.0.0.1:9000/ s3 mb s3://test
-      - name: Download Fuzz Targets
-        uses: actions/download-artifact@v3
-        with:
-          name: fuzz_targets
-          path: ./target
-      - name: Grant Execute Permissions
-        shell: bash
-        run: chmod +x ./target/${{ matrix.fuzz-targets }}
-      - name: Run Fuzz Test
-        shell: bash
-        run: ./target/${{ matrix.fuzz-targets }} -max_total_time=120
-        env:
-          OPENDAL_S3_TEST: on
-          OPENDAL_S3_BUCKET: test
-          OPENDAL_S3_ENDPOINT: "http://127.0.0.1:9000"
-          OPENDAL_S3_ACCESS_KEY_ID: minioadmin
-          OPENDAL_S3_SECRET_ACCESS_KEY: minioadmin
-          OPENDAL_S3_REGION: us-east-1
-
-  fuzz-test-fs:
-    runs-on: ubuntu-latest
-    needs: fuzz-build
-    strategy:
-      fail-fast: true
-      matrix:
-        fuzz-targets: [fuzz_reader, fuzz_reader_with_buffer, fuzz_writer]
-    steps:
-      - name: Install libfuzzer
-        run: sudo apt-get install -y libfuzzer-14-dev
-      - name: Download Fuzz Targets
-        uses: actions/download-artifact@v3
-        with:
-          name: fuzz_targets
-          path: ./target
-      - name: Grant Execute Permissions
-        shell: bash
-        run: chmod +x ./target/${{ matrix.fuzz-targets }}
-      - name: Run Fuzz Test
-        shell: bash
-        run: ./target/${{ matrix.fuzz-targets }} -max_total_time=120
-        env:
-          OPENDAL_FS_TEST: on
-          OPENDAL_FS_ROOT: ${{ runner.temp }}/
-
-  fuzz-test-memory:
-    runs-on: ubuntu-latest
-    needs: fuzz-build
-    strategy:
-      fail-fast: true
-      matrix:
-        fuzz-targets: [fuzz_reader, fuzz_reader_with_buffer, fuzz_writer]
-    steps:
-      - name: Install libfuzzer
-        run: sudo apt-get install -y libfuzzer-14-dev
-      - name: Download Fuzz Targets
-        uses: actions/download-artifact@v3
-        with:
-          name: fuzz_targets
-          path: ./target
-      - name: Grant Execute Permissions
-        shell: bash
-        run: chmod +x ./target/${{ matrix.fuzz-targets }}
-      - name: Run Fuzz Test
-        shell: bash
-        run: ./target/${{ matrix.fuzz-targets }} -max_total_time=120
-        env:
-          OPENDAL_MEMORY_TEST: on
-
-
-  fuzz-test-azure:
-    runs-on: ubuntu-latest
-    needs: fuzz-build
-    services:
-      azurite:
-        image: mcr.microsoft.com/azure-storage/azurite
-        ports:
-          - 10000:10000
-    strategy:
-      fail-fast: true
-      matrix:
-        fuzz-targets: [fuzz_reader, fuzz_reader_with_buffer, fuzz_writer]
-    steps:
-      - name: Install libfuzzer
-        run: sudo apt-get install -y libfuzzer-14-dev
-      - name: Setup test bucket
-        shell: bash
-        run: |
-          az storage container create \
-              --name test \
-              --connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
-      - name: Download Fuzz Targets
-        uses: actions/download-artifact@v3
-        with:
-          name: fuzz_targets
-          path: ./target
-      - name: Grant Execute Permissions
-        shell: bash
-        run: chmod +x ./target/${{ matrix.fuzz-targets }}
-      - name: Run Fuzz Test
-        shell: bash
-        run: ./target/${{ matrix.fuzz-targets }} -max_total_time=120
-        env:
-          OPENDAL_AZBLOB_TEST: on
-          OPENDAL_AZBLOB_CONTAINER: test
-          OPENDAL_AZBLOB_ENDPOINT: "http://127.0.0.1:10000/devstoreaccount1"
-          OPENDAL_AZBLOB_ACCOUNT_NAME: devstoreaccount1
-          OPENDAL_AZBLOB_ACCOUNT_KEY: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
-
+          setup: ${{ matrix.cases.setup }}
+          service: ${{ matrix.cases.service }}
+          target: ${{ matrix.target }}
diff --git a/Cargo.lock b/Cargo.lock
index 4912f1cea2..ba191452b0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4669,6 +4669,7 @@ dependencies = [
  "libfuzzer-sys",
  "opendal",
  "tokio",
+ "tracing",
  "tracing-subscriber",
  "uuid",
 ]
diff --git a/core/fuzz/Cargo.toml b/core/fuzz/Cargo.toml
index e6b34e8790..280846aba3 100644
--- a/core/fuzz/Cargo.toml
+++ b/core/fuzz/Cargo.toml
@@ -38,12 +38,9 @@ tracing-subscriber = { version = "0.3", features = [
   "env-filter",
   "tracing-log",
 ] }
+tracing = "0.1"
 uuid = { version = "1", features = ["v4"] }
 
-[[bin]]
-name = "fuzz_reader_with_buffer"
-path = "fuzz_reader_with_buffer.rs"
-
 [[bin]]
 name = "fuzz_reader"
 path = "fuzz_reader.rs"
diff --git a/core/fuzz/fuzz_reader.rs b/core/fuzz/fuzz_reader.rs
index b0f83fc03b..2d9c0efa84 100644
--- a/core/fuzz/fuzz_reader.rs
+++ b/core/fuzz/fuzz_reader.rs
@@ -31,6 +31,7 @@ use opendal::raw::tests::TEST_RUNTIME;
 use opendal::raw::BytesRange;
 use opendal::Operator;
 use opendal::Result;
+use tracing::warn;
 
 const MAX_DATA_SIZE: usize = 16 * 1024 * 1024;
 
@@ -39,6 +40,7 @@ struct FuzzInput {
     path: String,
     size: usize,
     range: BytesRange,
+    buffer: Option<usize>,
     actions: Vec<ReadAction>,
 }
 
@@ -53,6 +55,7 @@ impl Debug for FuzzInput {
             .field("path", &self.path)
             .field("size", &self.size)
             .field("range", &self.range.to_string())
+            .field("buffer", &self.buffer)
             .field("actions", &actions)
             .finish()
     }
@@ -83,6 +86,12 @@ impl Arbitrary<'_> for FuzzInput {
         };
         let range = BytesRange::new(offset, size);
 
+        let buffer = if u.int_in_range(0..=1)? == 1 {
+            Some(u.int_in_range(1..=8 * 1024 * 1024)?)
+        } else {
+            None
+        };
+
         let count = u.int_in_range(1..=1024)?;
         let mut actions = vec![];
 
@@ -121,6 +130,7 @@ impl Arbitrary<'_> for FuzzInput {
             path: uuid::Uuid::new_v4().to_string(),
             size: total_size,
             range,
+            buffer,
             actions,
         })
     }
@@ -130,10 +140,12 @@ async fn fuzz_reader(op: Operator, input: FuzzInput) -> Result<()> {
     let mut checker = ReadChecker::new(input.size, input.range);
     op.write(&input.path, checker.data()).await?;
 
-    let r = op
-        .reader_with(&input.path)
-        .range(input.range.to_range())
-        .await?;
+    let mut r = op.reader_with(&input.path);
+    r = r.range(input.range.to_range());
+    if let Some(buffer) = input.buffer {
+        r = r.buffer(buffer);
+    }
+    let r = r.await?;
 
     checker.check(r, &input.actions).await;
 
@@ -150,6 +162,11 @@ fuzz_target!(|input: FuzzInput| {
 
     let op = init_test_service().expect("operator init must succeed");
     if let Some(op) = op {
+        if !op.info().full_capability().read_with_range {
+            warn!("service doesn't support read with range, skip fuzzing");
+            return;
+        }
+
         TEST_RUNTIME.block_on(async {
             fuzz_reader(op, input.clone())
                 .await
diff --git a/core/fuzz/fuzz_reader_with_buffer.rs b/core/fuzz/fuzz_reader_with_buffer.rs
deleted file mode 100644
index aa1baf6f83..0000000000
--- a/core/fuzz/fuzz_reader_with_buffer.rs
+++ /dev/null
@@ -1,164 +0,0 @@
-// 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.
-
-#![no_main]
-
-use std::fmt::Debug;
-use std::fmt::Formatter;
-use std::io::SeekFrom;
-
-use libfuzzer_sys::arbitrary::Arbitrary;
-use libfuzzer_sys::arbitrary::Unstructured;
-use libfuzzer_sys::fuzz_target;
-use opendal::raw::tests::init_test_service;
-use opendal::raw::tests::ReadAction;
-use opendal::raw::tests::ReadChecker;
-use opendal::raw::tests::TEST_RUNTIME;
-use opendal::raw::BytesRange;
-use opendal::Operator;
-use opendal::Result;
-
-const MAX_DATA_SIZE: usize = 16 * 1024 * 1024;
-
-#[derive(Clone)]
-struct FuzzInput {
-    path: String,
-    size: usize,
-    range: BytesRange,
-    actions: Vec<ReadAction>,
-    buffer: usize,
-}
-
-impl Debug for FuzzInput {
-    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        let mut actions = self.actions.clone();
-        // Remove all Read(0) entry.
-        let empty = ReadAction::Read(0);
-        actions.retain(|e| e != &empty);
-
-        f.debug_struct("FuzzInput")
-            .field("path", &self.path)
-            .field("size", &self.size)
-            .field("buffer", &self.buffer)
-            .field("range", &self.range.to_string())
-            .field("actions", &actions)
-            .finish()
-    }
-}
-
-impl Arbitrary<'_> for FuzzInput {
-    fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
-        let total_size = u.int_in_range(1..=MAX_DATA_SIZE)?;
-        let buffer = u.int_in_range(1..=MAX_DATA_SIZE + 1024)?;
-
-        // TODO: it's valid that size is larger than total_size.
-        let (offset, size) = match u.int_in_range(0..=3)? {
-            // Full range
-            0 => (None, None),
-            1 => {
-                let offset = u.int_in_range(0..=total_size as u64 - 1)?;
-                (Some(offset), None)
-            }
-            2 => {
-                let size = u.int_in_range(1..=total_size as u64)?;
-                (None, Some(size))
-            }
-            3 => {
-                let offset = u.int_in_range(0..=total_size as u64 - 1)?;
-                let size = u.int_in_range(1..=total_size as u64 - offset)?;
-                (Some(offset), Some(size))
-            }
-            _ => unreachable!("invalid int generated by arbitrary"),
-        };
-        let range = BytesRange::new(offset, size);
-
-        let count = u.int_in_range(1..=1024)?;
-        let mut actions = vec![];
-
-        for _ in 0..count {
-            let action = match u.int_in_range(0..=4)? {
-                // Read
-                0 => {
-                    let size = u.int_in_range(0..=total_size * 2)?;
-                    ReadAction::Read(size)
-                }
-                // Next
-                1 => ReadAction::Next,
-                // Seek Start
-                2 => {
-                    // NOTE: seek out of the end of file is valid.
-                    let offset = u.int_in_range(0..=total_size * 2)?;
-                    ReadAction::Seek(SeekFrom::Start(offset as u64))
-                }
-                // Seek Current
-                3 => {
-                    let offset = u.int_in_range(-(total_size as i64)..=(total_size as i64))?;
-                    ReadAction::Seek(SeekFrom::Current(offset))
-                }
-                // Seek End
-                4 => {
-                    let offset = u.int_in_range(-(total_size as i64)..=(total_size as i64))?;
-                    ReadAction::Seek(SeekFrom::End(offset))
-                }
-                _ => unreachable!("invalid int generated by arbitrary"),
-            };
-
-            actions.push(action);
-        }
-
-        Ok(FuzzInput {
-            path: uuid::Uuid::new_v4().to_string(),
-            size: total_size,
-            range,
-            actions,
-            buffer,
-        })
-    }
-}
-
-async fn fuzz_reader_with_buffer(op: Operator, input: FuzzInput) -> Result<()> {
-    let mut checker = ReadChecker::new(input.size, input.range);
-    op.write(&input.path, checker.data()).await?;
-
-    let r = op
-        .reader_with(&input.path)
-        .range(input.range.to_range())
-        .buffer(input.buffer)
-        .await?;
-
-    checker.check(r, &input.actions).await;
-
-    op.delete(&input.path).await?;
-    Ok(())
-}
-
-fuzz_target!(|input: FuzzInput| {
-    let _ = tracing_subscriber::fmt()
-        .pretty()
-        .with_test_writer()
-        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
-        .try_init();
-
-    let op = init_test_service().expect("operator init must succeed");
-    if let Some(op) = op {
-        TEST_RUNTIME.block_on(async {
-            fuzz_reader_with_buffer(op, input.clone())
-                .await
-                .unwrap_or_else(|err| panic!("fuzz reader must succeed: {err:?}"));
-        })
-    }
-});
diff --git a/core/fuzz/fuzz_writer.rs b/core/fuzz/fuzz_writer.rs
index df40e84590..b84b1ee4a6 100644
--- a/core/fuzz/fuzz_writer.rs
+++ b/core/fuzz/fuzz_writer.rs
@@ -26,6 +26,7 @@ use opendal::raw::tests::WriteChecker;
 use opendal::raw::tests::TEST_RUNTIME;
 use opendal::Operator;
 use opendal::Result;
+use tracing::warn;
 
 const MAX_DATA_SIZE: usize = 16 * 1024 * 1024;
 
@@ -111,6 +112,11 @@ fuzz_target!(|input: FuzzInput| {
 
     let op = init_test_service().expect("operator init must succeed");
     if let Some(op) = op {
+        if !op.info().full_capability().write_can_multi {
+            warn!("service doesn't support write multi, skip fuzzing");
+            return;
+        }
+
         TEST_RUNTIME.block_on(async {
             fuzz_writer(op, input.clone())
                 .await