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