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

erickguan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 757b1f4b3 chore(bindings/ruby): use service tests scheme for ruby 
binding tests (#7635)
757b1f4b3 is described below

commit 757b1f4b3be55f844c75f1e5f77b8fb44ebd6bc7
Author: Erick Guan <[email protected]>
AuthorDate: Fri May 29 12:00:33 2026 +0800

    chore(bindings/ruby): use service tests scheme for ruby binding tests 
(#7635)
    
    * Add service tests to ruby binding
    
    * Reduce tests and lint 4.0
    
    * fixup! Add service tests to ruby binding
    
    * Bump dependency
    
    * Start to run Ruby tests
    
    * Test service independently
    
    * Reduce services tested
    
    * Fetch config from env
    
    * Add capability checks
    
    * rollback unwanted changes
---
 .../actions/test_behavior_binding_ruby/action.yaml |  50 ++++++
 .github/scripts/test_behavior/plan.py              |  25 ++-
 .github/workflows/ci_bindings_ruby.yml             |  11 +-
 .github/workflows/test_behavior.yml                |  14 ++
 .github/workflows/test_behavior_binding_ruby.yml   | 107 +++++++++++++
 bindings/ruby/Gemfile                              |  10 +-
 bindings/ruby/README.md                            |   5 +-
 bindings/ruby/Rakefile                             |  16 +-
 ...ocking_op_test.rb => blocking_operator_test.rb} |   2 +-
 .../ruby/test/service/blocking_operator_test.rb    | 175 +++++++++++++++++++++
 10 files changed, 397 insertions(+), 18 deletions(-)

diff --git a/.github/actions/test_behavior_binding_ruby/action.yaml 
b/.github/actions/test_behavior_binding_ruby/action.yaml
new file mode 100644
index 000000000..4bc017023
--- /dev/null
+++ b/.github/actions/test_behavior_binding_ruby/action.yaml
@@ -0,0 +1,50 @@
+# 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: Test Binding Ruby
+description: 'Test Core with given setup and service'
+inputs:
+  setup:
+    description: "The setup action for test"
+  service:
+    description: "The service to test"
+  feature:
+    description: "The feature to test"
+
+runs:
+  using: "composite"
+  steps:
+    - name: Setup
+      shell: bash
+      run: |
+        mkdir -p ./dynamic_test_binding_ruby &&
+        cat <<EOF >./dynamic_test_binding_ruby/action.yml
+        runs:
+          using: composite
+          steps:
+          - name: Setup Test
+            uses: ./.github/services/${{ inputs.service }}/${{ inputs.setup }}
+          - name: Run Test Binding Ruby
+            shell: bash
+            working-directory: bindings/ruby
+            run: |
+              bundle exec rake test:service
+            env:
+              OPENDAL_TEST: ${{ inputs.service }}
+        EOF
+    - name: Run
+      uses: ./dynamic_test_binding_ruby
diff --git a/.github/scripts/test_behavior/plan.py 
b/.github/scripts/test_behavior/plan.py
index 73be90bf1..3481be704 100755
--- a/.github/scripts/test_behavior/plan.py
+++ b/.github/scripts/test_behavior/plan.py
@@ -31,7 +31,7 @@ GITHUB_DIR = SCRIPT_PATH.parent.parent
 # The project dir for opendal.
 PROJECT_DIR = GITHUB_DIR.parent
 
-LANGUAGE_BINDING = ["java", "python", "nodejs", "go", "c", "cpp", "dotnet"]
+LANGUAGE_BINDING = ["java", "python", "ruby", "nodejs", "go", "c", "cpp", 
"dotnet"]
 
 INTEGRATIONS = ["object_store"]
 
@@ -82,6 +82,8 @@ class Hint:
     binding_java: bool = field(default=False, init=False)
     # Is binding python affected?
     binding_python: bool = field(default=False, init=False)
+    # Is binding ruby affected?
+    binding_ruby: bool = field(default=False, init=False)
     # Is binding nodejs affected?
     binding_nodejs: bool = field(default=False, init=False)
     # Is binding go affected?
@@ -291,6 +293,27 @@ def generate_language_binding_cases(
             # opendal-go-services doesn't provide TOS yet.
             "tos",
         ]]
+    
+    # Enable integrated services (cases) for ruby.
+    # Ruby binding only integrates some services. Read more in 
bindings/ruby/Cargo.toml
+    if language == "ruby":
+        cases = [v for v in cases if v["service"] in [
+            "azblob",
+            "azdls",
+            "cos",
+            "fs",
+            "gcs",
+            "ghac",
+            "http",
+            "ipmfs",
+            "memory",
+            "obs",
+            "oss",
+            "s3",
+            "webdav",
+            "webhdfs",
+            "azfile",
+        ]]
 
     if os.getenv("GITHUB_IS_PUSH") == "true":
         return cases
diff --git a/.github/workflows/ci_bindings_ruby.yml 
b/.github/workflows/ci_bindings_ruby.yml
index 231bd7907..08f018fd1 100644
--- a/.github/workflows/ci_bindings_ruby.yml
+++ b/.github/workflows/ci_bindings_ruby.yml
@@ -43,11 +43,6 @@ jobs:
   test:
     runs-on: ubuntu-latest
 
-    strategy:
-      fail-fast: false
-      matrix:
-        ruby-version: ["3.2", "3.3", "3.4"]
-
     env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all 
steps
       BUNDLE_GEMFILE: ${{ github.workspace }}/bindings/ruby/Gemfile
 
@@ -56,8 +51,8 @@ jobs:
 
       - uses: ruby/setup-ruby@v1
         with:
-          ruby-version: ${{ matrix.ruby-version }}
           bundler-cache: true # runs 'bundle install' and caches installed 
gems automatically
+          working-directory: bindings/ruby
 
       - name: Setup Rust toolchain
         uses: ./.github/actions/setup
@@ -67,7 +62,7 @@ jobs:
         run: |
           cargo clippy -- -D warnings
 
-      - name: Run tests and lint
+      - name: Run lint
         working-directory: bindings/ruby
         run: |
-          bundle exec rake
+          bundle exec rake standard
diff --git a/.github/workflows/test_behavior.yml 
b/.github/workflows/test_behavior.yml
index c7d2b52a1..2beafae43 100644
--- a/.github/workflows/test_behavior.yml
+++ b/.github/workflows/test_behavior.yml
@@ -133,6 +133,20 @@ jobs:
     with:
       os: ${{ matrix.os }}
       cases: ${{ toJson(matrix.cases) }}
+  
+  test_binding_ruby:
+    name: binding_ruby / ${{ matrix.os }}
+    needs: [ plan ]
+    if: fromJson(needs.plan.outputs.plan).components.binding_ruby
+    secrets: inherit
+    strategy:
+      fail-fast: false
+      matrix:
+        include: ${{ fromJson(needs.plan.outputs.plan).binding_ruby }}
+    uses: ./.github/workflows/test_behavior_binding_ruby.yml
+    with:
+      os: ${{ matrix.os }}
+      cases: ${{ toJson(matrix.cases) }}
 
   test_binding_nodejs:
     name: binding_nodejs / ${{ matrix.os }}
diff --git a/.github/workflows/test_behavior_binding_ruby.yml 
b/.github/workflows/test_behavior_binding_ruby.yml
new file mode 100644
index 000000000..61e3bfa18
--- /dev/null
+++ b/.github/workflows/test_behavior_binding_ruby.yml
@@ -0,0 +1,107 @@
+# 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: Behavior Test Binding Ruby
+
+on:
+  workflow_call:
+    inputs:
+      os:
+        required: true
+        type: string
+      cases:
+        required: true
+        type: string
+
+defaults:
+  run:
+    working-directory: bindings/ruby
+
+jobs:
+  test_base:
+    name: base
+    runs-on: ${{ inputs.os }}
+    
+    strategy:
+      fail-fast: false
+      matrix:
+        ruby-version: ["3.2", "3.3", "3.4", "4.0"]
+
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all 
steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/bindings/ruby/Gemfile
+
+    steps:
+      - uses: actions/checkout@v6
+      - name: Setup Rust toolchain
+        uses: ./.github/actions/setup
+        with:
+          need-nextest: true
+          need-protoc: true
+          need-rocksdb: true
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Setup Ruby and install dependencies
+        uses: ruby/setup-ruby@v1
+        with:
+          bundler-cache: true
+          working-directory: bindings/ruby # must repeat because GitHub 
Actions will not use defaults.run
+
+      - name: Test base
+        run: |
+          bundle exec rake test:base
+
+  test_service:
+    name: ${{ matrix.cases.service }} / ${{ matrix.cases.setup }}
+    runs-on: ${{ inputs.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        cases: ${{ fromJson(inputs.cases) }}
+      
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all 
steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/bindings/ruby/Gemfile
+
+    steps:
+      - uses: actions/checkout@v6
+      - name: Setup Rust toolchain
+        uses: ./.github/actions/setup
+        with:
+          need-nextest: true
+          need-protoc: true
+          need-rocksdb: true
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Setup 1Password Connect
+        shell: bash
+        run: |
+          echo "::add-mask::${{ secrets.OP_CONNECT_HOST }}"
+          echo "::add-mask::${{ secrets.OP_CONNECT_TOKEN }}"
+          echo "OP_CONNECT_HOST=${{ secrets.OP_CONNECT_HOST }}" >> 
"$GITHUB_ENV"
+          echo "OP_CONNECT_TOKEN=${{ secrets.OP_CONNECT_TOKEN }}" >> 
"$GITHUB_ENV"
+
+      - name: Setup Ruby and install dependencies
+        uses: ruby/setup-ruby@v1
+        with:
+          bundler-cache: true
+          working-directory: bindings/ruby # must repeat because GitHub 
Actions will not use defaults.run
+
+      - name: Test service
+        uses: ./.github/actions/test_behavior_binding_ruby
+        with:
+          setup: ${{ matrix.cases.setup }}
+          service: ${{ matrix.cases.service }}
+          feature: ${{ matrix.cases.feature }}
diff --git a/bindings/ruby/Gemfile b/bindings/ruby/Gemfile
index 7b9d60ead..f4e98957f 100644
--- a/bindings/ruby/Gemfile
+++ b/bindings/ruby/Gemfile
@@ -24,12 +24,12 @@ gemspec
 
 group :development, :test do
   gem "rake", ">= 13.2"
-  gem "rb_sys", "~> 0.9.110" # for Makefile generation in extconf.rb
+  gem "rb_sys", "~> 0.9.128" # for Makefile generation in extconf.rb
   gem "rake-compiler", "~> 1.2.9" # to build a debug build
-  gem "minitest", "~> 5.25.0" # test library
+  gem "minitest", "~> 6.0.6" # test library
   gem "minitest-reporters", "~> 1.7.1" # better test output
-  gem "activesupport", "~> 8.0.1" # testing support
-  gem "standard", "~> 1.44" # linter with pre-specified rules
+  gem "activesupport", "~> 8.0.5" # testing support
+  gem "standard", "~> 1.54" # linter with pre-specified rules
   gem "redcarpet", "~> 3.6" # for documentation markdown parsing
-  gem "yard-rustdoc", "~> 0.4.1" # to generate YARD documentation from Ruby 
and Rust source
+  gem "yard-rustdoc", "~> 0.4.2" # to generate YARD documentation from Ruby 
and Rust source
 end
diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md
index 54d96b9b4..8e74e847d 100644
--- a/bindings/ruby/README.md
+++ b/bindings/ruby/README.md
@@ -143,7 +143,10 @@ bundle exec rake compile
 Run tests:
 
 ```shell
-bundle exec rake test
+bundle exec rake test_base
+
+# Optional
+OPENDAL_TEST=fs bundle exec rake test_service
 ```
 
 Run linters:
diff --git a/bindings/ruby/Rakefile b/bindings/ruby/Rakefile
index 135bfe449..5ff06790d 100644
--- a/bindings/ruby/Rakefile
+++ b/bindings/ruby/Rakefile
@@ -75,12 +75,24 @@ end
 
 Rake::Task[:test].prerequisites << :compile
 
-Rake::TestTask.new do |t|
+Rake::TestTask.new("test:base") do |t|
   t.libs << "lib"
   t.libs << "test"
-  t.pattern = "test/**/*_test.rb"
+  t.test_files = FileList["test/*_test.rb"]
 end
 
+Rake::TestTask.new("test:service") do |t|
+  t.libs << "lib"
+  t.libs << "test"
+  t.test_files = FileList["test/service/*_test.rb"]
+end
+
+desc "Test base functionality"
+task "test:base" => :test
+
+desc "Test service functionality"
+task "test:service" => :test
+
 namespace :doc do
   task default: %i[rustdoc yard]
 
diff --git a/bindings/ruby/test/blocking_op_test.rb 
b/bindings/ruby/test/blocking_operator_test.rb
similarity index 98%
rename from bindings/ruby/test/blocking_op_test.rb
rename to bindings/ruby/test/blocking_operator_test.rb
index 079669241..31315bff7 100644
--- a/bindings/ruby/test/blocking_op_test.rb
+++ b/bindings/ruby/test/blocking_operator_test.rb
@@ -20,7 +20,7 @@
 require "test_helper"
 require "tmpdir"
 
-class OpenDalTest < ActiveSupport::TestCase
+class BlockingOperatorTest < ActiveSupport::TestCase
   setup do
     @root = Dir.mktmpdir
     File.write("#{@root}/sample", "Sample data for testing")
diff --git a/bindings/ruby/test/service/blocking_operator_test.rb 
b/bindings/ruby/test/service/blocking_operator_test.rb
new file mode 100644
index 000000000..ef5f582b1
--- /dev/null
+++ b/bindings/ruby/test/service/blocking_operator_test.rb
@@ -0,0 +1,175 @@
+# 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.
+
+# frozen_string_literal: true
+
+require "test_helper"
+
+SERVICE = ENV["OPENDAL_TEST"]
+
+module Service
+  class BlockingOperatorTest < ActiveSupport::TestCase
+    setup do
+      config = config_from_env(SERVICE)
+
+      @op = OpenDal::Operator.new(SERVICE, config)
+    end
+
+    test "stat empty path" do
+      skip("Not supported") if [email protected]
+
+      exception = assert_raises(RuntimeError) do
+        @op.stat("/invalid-path")
+      end
+
+      assert exception.message.start_with?("NotFound")
+    end
+
+    test "read and write text" do
+      skip("Not supported") if !(@op.capability.write && @op.capability.read)
+
+      @op.write("content", "OpenDAL Ruby is ready.")
+
+      content = @op.read("content")
+
+      assert_equal "OpenDAL Ruby is ready.", content
+    end
+
+    test "read and write binary" do
+      skip("Not supported") if !(@op.capability.write && @op.capability.read)
+
+      # writes 32-bit signed integers
+      @op.write("binary", [67305985, -50462977].pack("l*"))
+
+      bin = @op.read("binary")
+
+      assert_equal [67305985, -50462977], bin.unpack("l*")
+    end
+
+    test "stat returns file metadata" do
+      skip("Not supported") if !(@op.capability.write && @op.capability.stat)
+
+      @op.write("test_stat", "test")
+
+      stat = @op.stat("test_stat")
+
+      assert stat.is_a?(OpenDal::Metadata)
+      assert_equal 4, stat.content_length
+      assert stat.file?
+    end
+
+    test "create_dir creates directory" do
+      skip("Not supported") if !(@op.capability.create_dir && 
@op.capability.stat)
+
+      @op.create_dir("new/directory/")
+
+      assert_equal @op.stat("new/directory/").mode, 
OpenDal::Metadata::DIRECTORY
+    end
+
+    test "exists returns existence" do
+      skip("Not supported") if !(@op.capability.read && @op.capability.write 
&& @op.capability.create_dir)
+
+      @op.write("exist_file", "test")
+      @op.create_dir("exist/directory/")
+
+      assert @op.exist?("exist_file")
+      assert @op.exist?("exist/directory/")
+    end
+
+    test "delete removes file" do
+      skip("Not supported") if !(@op.capability.delete && @op.capability.write 
&& @op.capability.stat)
+
+      @op.write("deletion_test", "test")
+
+      assert @op.exist?("deletion_test"), "expect file to exist before 
deletion"
+
+      @op.delete("deletion_test")
+      assert [email protected]?("/deletion_test"), "expect file not to exist after 
deletion"
+    end
+
+    test "rename renames file" do
+      skip("Not supported") if !(@op.capability.rename && @op.capability.write 
&& @op.capability.stat)
+
+      @op.write("rename_test", "test")
+
+      @op.rename("rename_test", "new_name")
+      assert [email protected]?("rename_test"), "expect file not to exist after 
renaming"
+      assert @op.exist?("new_name"), "expect file exist after renaming"
+    end
+
+    test "remove_all removes files" do
+      skip("Not supported") if !(@op.capability.delete && 
@op.capability.create_dir)
+
+      @op.create_dir("nested/directory/")
+      @op.write("nested/directory/text", "content")
+
+      @op.remove_all("nested")
+      assert [email protected]?("nested/directory/"), "expect file not to exist after 
removal"
+    end
+
+    test "copy copies file" do
+      skip("Not supported") if !(@op.capability.write && @op.capability.read 
&& @op.capability.copy)
+
+      @op.write("copy_test", "test")
+      @op.copy("copy_test", "copy_destination")
+
+      assert @op.exist?("copy_test"), "expect source file to exist"
+      assert @op.exist?("copy_destination"), "expect destination file to exist"
+    end
+
+    test "opens an IO" do
+      skip("Not supported") if !(@op.capability.write && @op.capability.read)
+
+      @op.write("io_test", "test")
+
+      io = @op.open("/io_test", "rb")
+      content = io.read
+
+      assert_not io.closed?
+      assert_equal "test", content
+
+      io.close
+    end
+
+    test "middleware applies a middleware" do
+      @op.middleware(OpenDal::Middleware::Retry.new)
+
+      assert @op.is_a?(OpenDal::Operator)
+    end
+
+    private
+
+    def config_from_env(service)
+      prefix = "OPENDAL_#{service.upcase}_"
+      config = {}
+      ENV.each do |key, value|
+        if key.start_with?(prefix)
+          config[key[prefix.length..].downcase] = value
+        end
+      end
+
+      disable_random_root = ENV["OPENDAL_DISABLE_RANDOM_ROOT"] == "true"
+      unless disable_random_root
+        root = config.fetch("root", "/")
+        uuid = SecureRandom.uuid
+        config["root"] = "#{root}/#{uuid}/"
+      end
+
+      config
+    end
+  end
+end

Reply via email to