Skip to content

Commit 6100a69

Browse files
authored
feat(bindings/dotnet): Bindings dotnet implement (#7238)
* feat(bindings/dotnet): Implement operator safety and expection handle * feat(bindings/dotnet): implement Operator bindings * feat(bindings/dotnet): complete .NET bindings with README and behavior tests * fix(bindings/dotnet): losing license header * fix(bindings/dotnet): rust code format * fix(bindings/dotnet): fix behavior tests by updating scripts and Cargo.toml * fix(bindings/dotnet): enhance BehaviorTestBase with lock management for sensitive services * feat(bindings/dotnet): refactor behavior tests to use BehaviorOperatorFixture and improve test structure * refactor(bindings/dotnet): consolidate tests and simplify CI test commands - Migrate legacy Fs/Memory operator coverage into behavior-oriented tests. - Add additional sync/async parity cases across behavior test suites. - Introduce OperatorBehaviorTest for shared operator scenarios (info, duplicate, stream roundtrip, and cancellation resilience). - Remove obsolete FsOperatorTest and MemoryOperatorTest after migration. - Simplify Dotnet CI test invocation by using OPENDAL_TEST=memory and a unified `dotnet test` command. - Simplify behavior binding action test command to reduce filter confusion. * fix(bindings/dotnet): add dedicated operator info tests to avoid service-specific behavior drift * fix(bindings/dotnet): fix append/list-limit behavior tests
1 parent aa212e1 commit 6100a69

File tree

149 files changed

+16078
-159
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

149 files changed

+16078
-159
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
name: Test Binding Dotnet
19+
description: 'Test Dotnet binding with given setup and service'
20+
inputs:
21+
setup:
22+
description: "The setup action for test"
23+
service:
24+
description: "The service to test"
25+
feature:
26+
description: "The feature to test"
27+
28+
runs:
29+
using: "composite"
30+
steps:
31+
- name: Setup
32+
shell: bash
33+
run: |
34+
mkdir -p ./dynamic_test_binding_dotnet &&
35+
cat <<EOF >./dynamic_test_binding_dotnet/action.yml
36+
runs:
37+
using: composite
38+
steps:
39+
- name: Setup Test
40+
uses: ./.github/services/${{ inputs.service }}/${{ inputs.setup }}
41+
- name: Run Test Binding Dotnet
42+
shell: bash
43+
working-directory: bindings/dotnet
44+
run: |
45+
cargo build
46+
dotnet test -f net10.0
47+
env:
48+
OPENDAL_TEST: ${{ inputs.service }}
49+
EOF
50+
- name: Run
51+
uses: ./dynamic_test_binding_dotnet

.github/scripts/test_behavior/plan.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
# The project dir for opendal.
3232
PROJECT_DIR = GITHUB_DIR.parent
3333

34-
LANGUAGE_BINDING = ["java", "python", "nodejs", "go", "c", "cpp"]
34+
LANGUAGE_BINDING = ["java", "python", "nodejs", "go", "c", "cpp", "dotnet"]
3535

3636
INTEGRATIONS = ["object_store"]
3737

@@ -90,6 +90,8 @@ class Hint:
9090
binding_c: bool = field(default=False, init=False)
9191
# Is binding cpp affected?
9292
binding_cpp: bool = field(default=False, init=False)
93+
# Is binding dotnet affected?
94+
binding_dotnet: bool = field(default=False, init=False)
9395
# Is integration object_store affected ?
9496
integration_object_store: bool = field(default=False, init=False)
9597

@@ -153,12 +155,8 @@ def mark_service_affected(service: str) -> None:
153155
and not p.startswith("core/core/src/docs/")
154156
):
155157
hint.core = True
156-
hint.binding_java = True
157-
hint.binding_python = True
158-
hint.binding_nodejs = True
159-
hint.binding_go = True
160-
hint.binding_c = True
161-
hint.binding_cpp = True
158+
for language in LANGUAGE_BINDING:
159+
setattr(hint, f"binding_{language}", True)
162160
for integration in INTEGRATIONS:
163161
setattr(hint, f"integration_{integration}", True)
164162
hint.all_service = True
@@ -277,6 +275,14 @@ def generate_language_binding_cases(
277275
"rocksdb",
278276
]]
279277

278+
# Remove invalid cases for dotnet.
279+
if language == "dotnet":
280+
cases = [v for v in cases if v["service"] not in [
281+
"hdfs",
282+
"hdfs_native",
283+
"rocksdb",
284+
]]
285+
280286
if os.getenv("GITHUB_IS_PUSH") == "true":
281287
return cases
282288

.github/workflows/ci_bindings_dotnet.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,5 @@ jobs:
9292
run: |
9393
cargo build
9494
dotnet test -f ${{ matrix.dotnet-version == '8.0.x' && 'net8.0' || 'net10.0' }}
95+
env:
96+
OPENDAL_TEST: memory

.github/workflows/test_behavior.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,20 @@ jobs:
190190
os: ${{ matrix.os }}
191191
cases: ${{ toJson(matrix.cases) }}
192192

193+
test_binding_dotnet:
194+
name: binding_dotnet / ${{ matrix.os }}
195+
needs: [ plan ]
196+
if: fromJson(needs.plan.outputs.plan).components.binding_dotnet
197+
secrets: inherit
198+
strategy:
199+
fail-fast: false
200+
matrix:
201+
include: ${{ fromJson(needs.plan.outputs.plan).binding_dotnet }}
202+
uses: ./.github/workflows/test_behavior_binding_dotnet.yml
203+
with:
204+
os: ${{ matrix.os }}
205+
cases: ${{ toJson(matrix.cases) }}
206+
193207

194208
test_integration_object_store:
195209
name: integration_object_store / ${{ matrix.os }}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
name: Behavior Test Binding Dotnet
19+
20+
on:
21+
workflow_call:
22+
inputs:
23+
os:
24+
required: true
25+
type: string
26+
cases:
27+
required: true
28+
type: string
29+
30+
jobs:
31+
test:
32+
name: ${{ matrix.cases.service }} / ${{ matrix.cases.setup }}
33+
runs-on: ${{ inputs.os }}
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
cases: ${{ fromJson(inputs.cases) }}
38+
steps:
39+
- uses: actions/checkout@v6
40+
- name: Setup dotnet toolchain
41+
uses: actions/setup-dotnet@v5
42+
with:
43+
dotnet-version: '10.0.x'
44+
- name: Setup Rust toolchain
45+
uses: ./.github/actions/setup
46+
with:
47+
need-nextest: true
48+
need-protoc: true
49+
need-rocksdb: true
50+
github-token: ${{ secrets.GITHUB_TOKEN }}
51+
52+
- name: Setup 1Password Connect
53+
uses: 1password/load-secrets-action/configure@v3
54+
with:
55+
connect-host: ${{ secrets.OP_CONNECT_HOST }}
56+
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
57+
58+
- name: Test Core
59+
uses: ./.github/actions/test_behavior_binding_dotnet
60+
with:
61+
setup: ${{ matrix.cases.setup }}
62+
service: ${{ matrix.cases.service }}
63+
feature: ${{ matrix.cases.feature }}

bindings/dotnet/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ opendal = { version = ">=0", path = "../../core", features = [
6060
"services-webhdfs",
6161
"services-aliyun-drive",
6262
"services-cacache",
63+
"services-compfs",
6364
"services-dashmap",
6465
"services-dropbox",
6566
"services-etcd",
@@ -100,3 +101,13 @@ opendal = { version = ">=0", path = "../../core", features = [
100101
"services-yandex-disk",
101102
] }
102103
tokio = { version = "1.49.0", features = ["full"] }
104+
105+
# This is not optimal. See also the Cargo issue:
106+
# https://github.com/rust-lang/cargo/issues/1197#issuecomment-1641086954
107+
[target.'cfg(unix)'.dependencies.opendal]
108+
features = [
109+
# Depend on unix-only dependency stacks in current implementations.
110+
"services-monoiofs",
111+
"services-sftp",
112+
]
113+
path = "../../core"

bindings/dotnet/DotOpenDAL.Tests/BlockingOperatorTest.cs renamed to bindings/dotnet/DotOpenDAL.Tests/Behavior/BehaviorOperatorCollection.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,7 @@
1919

2020
namespace DotOpenDAL.Tests;
2121

22-
public class BlockingOperatorTest
22+
[CollectionDefinition("BehaviorOperator", DisableParallelization = false)]
23+
public sealed class BehaviorOperatorCollection : ICollectionFixture<BehaviorOperatorFixture>
2324
{
24-
[Fact]
25-
public void TestReadWrite()
26-
{
27-
var op = new BlockingOperator();
28-
var content = "123456";
29-
Assert.NotEqual(op.Op, IntPtr.Zero);
30-
op.Write("test", content);
31-
var result = op.Read("test");
32-
Assert.Equal(content, result);
33-
}
3425
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
using System.Collections;
21+
22+
namespace DotOpenDAL.Tests;
23+
24+
public sealed class BehaviorOperatorFixture : IDisposable
25+
{
26+
private readonly Operator? op;
27+
28+
public string? Scheme { get; }
29+
30+
public BehaviorOperatorFixture()
31+
{
32+
Scheme = Environment.GetEnvironmentVariable("OPENDAL_TEST");
33+
if (string.IsNullOrWhiteSpace(Scheme))
34+
{
35+
return;
36+
}
37+
38+
var scheme = Scheme.ToLowerInvariant().Replace('_', '-');
39+
var options = BuildConfigFromEnvironment(Scheme);
40+
41+
// Align with other bindings: isolate behavior tests with random roots by default.
42+
if (!IsRandomRootDisabled())
43+
{
44+
var baseRoot = options.TryGetValue("root", out var root) && !string.IsNullOrWhiteSpace(root)
45+
? root!
46+
: "/";
47+
options["root"] = BuildRandomRoot(baseRoot);
48+
}
49+
50+
op = new Operator(scheme, options);
51+
}
52+
53+
public bool IsEnabled => op is not null;
54+
55+
public Operator Op => op ?? throw new InvalidOperationException("Behavior operator is not initialized.");
56+
57+
public Capability Capability => Op.Info.FullCapability;
58+
59+
public void Dispose()
60+
{
61+
op?.Dispose();
62+
}
63+
64+
private static Dictionary<string, string> BuildConfigFromEnvironment(string service)
65+
{
66+
var variables = Environment.GetEnvironmentVariables();
67+
var prefix = $"opendal_{service.ToLowerInvariant()}_";
68+
var config = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
69+
70+
foreach (DictionaryEntry entry in variables)
71+
{
72+
var key = entry.Key?.ToString();
73+
var value = entry.Value?.ToString();
74+
if (string.IsNullOrWhiteSpace(key) || value is null)
75+
{
76+
continue;
77+
}
78+
79+
var normalized = key.ToLowerInvariant();
80+
if (!normalized.StartsWith(prefix, StringComparison.Ordinal))
81+
{
82+
continue;
83+
}
84+
85+
config[normalized[prefix.Length..]] = value;
86+
}
87+
88+
return config;
89+
}
90+
91+
private static bool IsRandomRootDisabled()
92+
{
93+
return string.Equals(
94+
Environment.GetEnvironmentVariable("OPENDAL_DISABLE_RANDOM_ROOT"),
95+
"true",
96+
StringComparison.OrdinalIgnoreCase);
97+
}
98+
99+
private static string BuildRandomRoot(string baseRoot)
100+
{
101+
var trimmed = baseRoot.Trim();
102+
if (trimmed.Length == 0)
103+
{
104+
trimmed = "/";
105+
}
106+
107+
if (!trimmed.EndsWith('/'))
108+
{
109+
trimmed += "/";
110+
}
111+
112+
return $"{trimmed}{Guid.NewGuid():N}/";
113+
}
114+
}

0 commit comments

Comments
 (0)