Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/actions/test_behavior_binding_dotnet/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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 Dotnet
description: 'Test Dotnet binding 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_dotnet &&
cat <<EOF >./dynamic_test_binding_dotnet/action.yml
runs:
using: composite
steps:
- name: Setup Test
uses: ./.github/services/${{ inputs.service }}/${{ inputs.setup }}
- name: Run Test Binding Dotnet
shell: bash
working-directory: bindings/dotnet
run: |
cargo build
dotnet test --filter "FullyQualifiedName~Behavior" -f net10.0 --logger "console;verbosity=normal"
env:
OPENDAL_TEST: ${{ inputs.service }}
EOF
- name: Run
uses: ./dynamic_test_binding_dotnet
20 changes: 13 additions & 7 deletions .github/scripts/test_behavior/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
# The project dir for opendal.
PROJECT_DIR = GITHUB_DIR.parent

LANGUAGE_BINDING = ["java", "python", "nodejs", "go", "c", "cpp"]
LANGUAGE_BINDING = ["java", "python", "nodejs", "go", "c", "cpp", "dotnet"]

INTEGRATIONS = ["object_store"]

Expand Down Expand Up @@ -90,6 +90,8 @@ class Hint:
binding_c: bool = field(default=False, init=False)
# Is binding cpp affected?
binding_cpp: bool = field(default=False, init=False)
# Is binding dotnet affected?
binding_dotnet: bool = field(default=False, init=False)
# Is integration object_store affected ?
integration_object_store: bool = field(default=False, init=False)

Expand Down Expand Up @@ -153,12 +155,8 @@ def mark_service_affected(service: str) -> None:
and not p.startswith("core/core/src/docs/")
):
hint.core = True
hint.binding_java = True
hint.binding_python = True
hint.binding_nodejs = True
hint.binding_go = True
hint.binding_c = True
hint.binding_cpp = True
for language in LANGUAGE_BINDING:
setattr(hint, f"binding_{language}", True)
for integration in INTEGRATIONS:
setattr(hint, f"integration_{integration}", True)
hint.all_service = True
Expand Down Expand Up @@ -277,6 +275,14 @@ def generate_language_binding_cases(
"rocksdb",
]]

# Remove invalid cases for dotnet.
if language == "dotnet":
cases = [v for v in cases if v["service"] not in [
"hdfs",
"hdfs_native",
"rocksdb",
]]

if os.getenv("GITHUB_IS_PUSH") == "true":
return cases

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci_bindings_dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ jobs:
working-directory: "bindings/dotnet"
run: |
cargo build
dotnet test -f ${{ matrix.dotnet-version == '8.0.x' && 'net8.0' || 'net10.0' }}
dotnet test --filter "FullyQualifiedName!~Behavior" -f ${{ matrix.dotnet-version == '8.0.x' && 'net8.0' || 'net10.0' }}
Copy link
Member

@tisonkun tisonkun Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FullyQualifiedName~Behavior is for Test Binding Dotnet. [test_behavior_binding_dotnet/action.yaml]

FullyQualifiedName!~Behavior is for Bindings Dotnet CI. [ci_bindings_dotnet.yml]

one for behavior/integration coverage, and one for normal CI coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the test cases make the structure clearer and simplify the CI test commands.

14 changes: 14 additions & 0 deletions .github/workflows/test_behavior.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ jobs:
os: ${{ matrix.os }}
cases: ${{ toJson(matrix.cases) }}

test_binding_dotnet:
name: binding_dotnet / ${{ matrix.os }}
needs: [ plan ]
if: fromJson(needs.plan.outputs.plan).components.binding_dotnet
secrets: inherit
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.plan.outputs.plan).binding_dotnet }}
uses: ./.github/workflows/test_behavior_binding_dotnet.yml
with:
os: ${{ matrix.os }}
cases: ${{ toJson(matrix.cases) }}


test_integration_object_store:
name: integration_object_store / ${{ matrix.os }}
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/test_behavior_binding_dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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 Dotnet

on:
workflow_call:
inputs:
os:
required: true
type: string
cases:
required: true
type: string

jobs:
test:
name: ${{ matrix.cases.service }} / ${{ matrix.cases.setup }}
runs-on: ${{ inputs.os }}
strategy:
fail-fast: false
matrix:
cases: ${{ fromJson(inputs.cases) }}
steps:
- uses: actions/checkout@v6
- name: Setup dotnet toolchain
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- 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
uses: 1password/load-secrets-action/configure@v3
with:
connect-host: ${{ secrets.OP_CONNECT_HOST }}
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}

- name: Test Core
uses: ./.github/actions/test_behavior_binding_dotnet
with:
setup: ${{ matrix.cases.setup }}
service: ${{ matrix.cases.service }}
feature: ${{ matrix.cases.feature }}
11 changes: 11 additions & 0 deletions bindings/dotnet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ opendal = { version = ">=0", path = "../../core", features = [
"services-webhdfs",
"services-aliyun-drive",
"services-cacache",
"services-compfs",
"services-dashmap",
"services-dropbox",
"services-etcd",
Expand Down Expand Up @@ -100,3 +101,13 @@ opendal = { version = ">=0", path = "../../core", features = [
"services-yandex-disk",
] }
tokio = { version = "1.49.0", features = ["full"] }

# This is not optimal. See also the Cargo issue:
# https://github.com/rust-lang/cargo/issues/1197#issuecomment-1641086954
[target.'cfg(unix)'.dependencies.opendal]
features = [
# Depend on unix-only dependency stacks in current implementations.
"services-monoiofs",
"services-sftp",
]
path = "../../core"
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,7 @@

namespace DotOpenDAL.Tests;

public class BlockingOperatorTest
[CollectionDefinition("BehaviorOperator", DisableParallelization = false)]
public sealed class BehaviorOperatorCollection : ICollectionFixture<BehaviorOperatorFixture>
{
[Fact]
public void TestReadWrite()
{
var op = new BlockingOperator();
var content = "123456";
Assert.NotEqual(op.Op, IntPtr.Zero);
op.Write("test", content);
var result = op.Read("test");
Assert.Equal(content, result);
}
}
114 changes: 114 additions & 0 deletions bindings/dotnet/DotOpenDAL.Tests/Behavior/BehaviorOperatorFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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.
*/

using System.Collections;

namespace DotOpenDAL.Tests;

public sealed class BehaviorOperatorFixture : IDisposable
{
private readonly Operator? op;

public string? Scheme { get; }

public BehaviorOperatorFixture()
{
Scheme = Environment.GetEnvironmentVariable("OPENDAL_TEST");
if (string.IsNullOrWhiteSpace(Scheme))
{
return;
}

var scheme = Scheme.ToLowerInvariant().Replace('_', '-');
var options = BuildConfigFromEnvironment(Scheme);

// Align with other bindings: isolate behavior tests with random roots by default.
if (!IsRandomRootDisabled())
{
var baseRoot = options.TryGetValue("root", out var root) && !string.IsNullOrWhiteSpace(root)
? root!
: "/";
options["root"] = BuildRandomRoot(baseRoot);
}

op = new Operator(scheme, options);
}

public bool IsEnabled => op is not null;

public Operator Op => op ?? throw new InvalidOperationException("Behavior operator is not initialized.");

public Capability Capability => Op.Info.FullCapability;

public void Dispose()
{
op?.Dispose();
}

private static Dictionary<string, string> BuildConfigFromEnvironment(string service)
{
var variables = Environment.GetEnvironmentVariables();
var prefix = $"opendal_{service.ToLowerInvariant()}_";
var config = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

foreach (DictionaryEntry entry in variables)
{
var key = entry.Key?.ToString();
var value = entry.Value?.ToString();
if (string.IsNullOrWhiteSpace(key) || value is null)
{
continue;
}

var normalized = key.ToLowerInvariant();
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
{
continue;
}

config[normalized[prefix.Length..]] = value;
}

return config;
}

private static bool IsRandomRootDisabled()
{
return string.Equals(
Environment.GetEnvironmentVariable("OPENDAL_DISABLE_RANDOM_ROOT"),
"true",
StringComparison.OrdinalIgnoreCase);
}

private static string BuildRandomRoot(string baseRoot)
{
var trimmed = baseRoot.Trim();
if (trimmed.Length == 0)
{
trimmed = "/";
}

if (!trimmed.EndsWith('/'))
{
trimmed += "/";
}

return $"{trimmed}{Guid.NewGuid():N}/";
}
}
Loading
Loading