From d1bb109e7fa0126f8e606a5b035e6732a1202d89 Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Thu, 13 Nov 2025 22:06:24 -0300 Subject: [PATCH] Add testing for Csharp --- .github/workflows/validate.yaml | 29 +++- duper_uniffi/csharp/Duper.nuspec | 2 +- duper_uniffi/example/csharp/Example.cs | 2 +- duper_uniffi/example/csharp/Example.csproj | 30 ++++ duper_uniffi/example/csharp/example.csproj | 15 -- duper_uniffi/justfile | 13 +- duper_uniffi/tests/csharp/.gitignore | 2 + duper_uniffi/tests/csharp/Duper.Tests.csproj | 39 ++++++ .../tests/csharp/DuperSerializerTests.cs | 129 ++++++++++++++++++ duper_uniffi/tests/csharp/GlobalUsings.cs | 1 + 10 files changed, 240 insertions(+), 22 deletions(-) create mode 100644 duper_uniffi/example/csharp/Example.csproj delete mode 100644 duper_uniffi/example/csharp/example.csproj create mode 100644 duper_uniffi/tests/csharp/.gitignore create mode 100644 duper_uniffi/tests/csharp/Duper.Tests.csproj create mode 100644 duper_uniffi/tests/csharp/DuperSerializerTests.cs create mode 100644 duper_uniffi/tests/csharp/GlobalUsings.cs diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index b41a1ee..56933cb 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -15,6 +15,7 @@ env: CARGO_PROFILE_TEST_DEBUG: "0" PYTHON_MINIMUM_VERSION: "3.10" NODE_VERSION: "20" + DOTNET_VERSION: "9.0.x" jobs: rustfmt: @@ -115,7 +116,7 @@ jobs: python-version: ${{ env.PYTHON_MINIMUM_VERSION }} - name: Cache Rust dependencies uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 - - name: Test Python + - name: Test with pytest working-directory: ./duper-python run: | uv run maturin develop @@ -152,3 +153,29 @@ jobs: working-directory: ./duper-js-wasm run: | npm run test + test-csharp: + name: Run C# tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: Setup .NET + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Fetch MSRV + run: | + rustup toolchain install ${RUST_MSRV}-x86_64-unknown-linux-gnu + rustup override set ${RUST_MSRV}-x86_64-unknown-linux-gnu + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 + - name: Build Uniffi bindings + working-directory: ./duper_uniffi + run: | + cargo build + - name: Test with xUnit + working-directory: ./duper_uniffi/tests/csharp + run: | + dotnet test diff --git a/duper_uniffi/csharp/Duper.nuspec b/duper_uniffi/csharp/Duper.nuspec index dd7a248..769f551 100644 --- a/duper_uniffi/csharp/Duper.nuspec +++ b/duper_uniffi/csharp/Duper.nuspec @@ -21,7 +21,7 @@ - + diff --git a/duper_uniffi/example/csharp/Example.cs b/duper_uniffi/example/csharp/Example.cs index ef660dd..4f91940 100644 --- a/duper_uniffi/example/csharp/Example.cs +++ b/duper_uniffi/example/csharp/Example.cs @@ -26,7 +26,7 @@ namespace Example public bool DarkMode { get; set; } [Duper("Locale")] public required string @language { get; set; } - public Dictionary? @metadata { get; set; } + public Dictionary? @metadata { get; set; } } public class Example diff --git a/duper_uniffi/example/csharp/Example.csproj b/duper_uniffi/example/csharp/Example.csproj new file mode 100644 index 0000000..58a7a50 --- /dev/null +++ b/duper_uniffi/example/csharp/Example.csproj @@ -0,0 +1,30 @@ + + + + Exe + net8.0 + enable + enable + true + + false + ../../../target/debug + + + + + + + + + PreserveNewest + + + + + + PreserveNewest + + + + diff --git a/duper_uniffi/example/csharp/example.csproj b/duper_uniffi/example/csharp/example.csproj deleted file mode 100644 index 121bb3b..0000000 --- a/duper_uniffi/example/csharp/example.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net8.0 - enable - enable - true - - - - - - - diff --git a/duper_uniffi/justfile b/duper_uniffi/justfile index 514b1a8..e454d2d 100644 --- a/duper_uniffi/justfile +++ b/duper_uniffi/justfile @@ -12,13 +12,18 @@ build-release: cargo build --release --target x86_64-pc-windows-gnu [working-directory: 'csharp'] -package-cs: bindgen build-release +package-cs: build-release bindgen dotnet build dotnet pack -example: bindgen build-debug example-cs +example: build-debug bindgen example-cs [working-directory: 'example/csharp'] example-cs: - dotnet build - LD_LIBRARY_PATH="../../../target/debug" dotnet run + dotnet run + +test: build-debug bindgen test-cs + +[working-directory: 'tests/csharp'] +test-cs: + dotnet test diff --git a/duper_uniffi/tests/csharp/.gitignore b/duper_uniffi/tests/csharp/.gitignore new file mode 100644 index 0000000..cd42ee3 --- /dev/null +++ b/duper_uniffi/tests/csharp/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/duper_uniffi/tests/csharp/Duper.Tests.csproj b/duper_uniffi/tests/csharp/Duper.Tests.csproj new file mode 100644 index 0000000..4af2095 --- /dev/null +++ b/duper_uniffi/tests/csharp/Duper.Tests.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + enable + + false + true + ../../../target/debug + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + PreserveNewest + + + + + + PreserveNewest + + + + diff --git a/duper_uniffi/tests/csharp/DuperSerializerTests.cs b/duper_uniffi/tests/csharp/DuperSerializerTests.cs new file mode 100644 index 0000000..716eacd --- /dev/null +++ b/duper_uniffi/tests/csharp/DuperSerializerTests.cs @@ -0,0 +1,129 @@ +using System.Net; + +namespace Duper.Tests; + +public class DuperSerializerTests +{ + [Fact] + public void DuperSerializer_Deserialize_Null() + { + object? output = DuperSerializer.Deserialize("null"); + Assert.Null(output); + } + + [Fact] + public void DuperSerializer_Deserialize_Boolean() + { + bool output = DuperSerializer.Deserialize("true"); + Assert.True(output); + } + + [Fact] + public void DuperSerializer_Deserialize_Int() + { + int output = DuperSerializer.Deserialize("10"); + Assert.Equal(10, output); + } + + [Fact] + public void DuperSerializer_Deserialize_Long() + { + long output = DuperSerializer.Deserialize("12345678910"); + Assert.Equal(12345678910, output); + } + + [Fact] + public void DuperSerializer_Deserialize_Float() + { + float output = DuperSerializer.Deserialize("10.2"); + Assert.Equal(10.2, output, 0.0001); + } + + [Fact] + public void DuperSerializer_Deserialize_Double() + { + double output = DuperSerializer.Deserialize("2.7e+100"); + Assert.Equal(2.7e+100, output); + } + + [Fact] + public void DuperSerializer_Deserialize_String() + { + string? output = DuperSerializer.Deserialize(@"r""super duper"""); + Assert.Equal("super duper", output); + } + + [Fact] + public void DuperSerializer_Deserialize_Bytes() + { + byte[]? output = DuperSerializer.Deserialize(@"b""\x1b[0m"""); + Assert.Equal([27, 91, 48, 109], output); + } + + [Fact] + public void DuperSerializer_Deserialize_Tuple() + { + (string, object?) output = DuperSerializer.Deserialize<(string, object?)>(@"(""hello"", null)"); + Assert.Equal(("hello", null), output); + } + + [Duper("UserProfile")] + public class UserProfileExample + { + [Duper("Uuid")] + public required string @id; // Support for public fields, too + public required string @username { get; set; } + [Duper("EmailAddress")] + public required string @email { get; set; } + public required UserSettingsExample @settings { get; set; } + public float @score { get; set; } + [Duper("Png")] + public required byte[] @avatar { get; set; } + public string? @bio { get; set; } + [Duper(Key = "last_logins")] + public required IList<(System.Net.IPAddress, DateTimeOffset)> LastLogins { get; set; } + } + + public class UserSettingsExample + { + [Duper(Key = "dark mode")] + public bool DarkMode { get; set; } + [Duper("Locale")] + public required string @language { get; set; } + public Dictionary? @metadata { get; set; } + } + + [Fact] + public void DuperSerializer_Deserialize_FullExample() + { + UserProfileExample? userProfile = DuperSerializer.Deserialize(""" + UserProfile({ + id: Uuid("f111c275-b4ce-4392-8e5b-19067ce39b53"), + username: "EpicEric", + email: EmailAddress("eric@duper.dev.br"), + settings: { + "dark mode": true, + language: Locale("pt-BR"), + metadata: null, + }, + score: 120.25, + // Support for bytes, woohoo! + avatar: Png(b64"iVBORw0KGgoAAAANSUhEUgAAAGQ"), + bio: r#"Hello! I'm a super "duper" user!"#, + last_logins: [ + (IPv4Address("192.168.1.100"), Instant('2024-03-20T14:30:00+00:00')), + ], + }) + """); + Assert.NotNull(userProfile); + + Assert.Equal("f111c275-b4ce-4392-8e5b-19067ce39b53", userProfile.id); + Assert.Equal("EpicEric", userProfile.username); + Assert.Equal("eric@duper.dev.br", userProfile.email); + Assert.Equivalent(new UserSettingsExample { DarkMode = true, language = "pt-BR", metadata = null }, userProfile.settings); + Assert.Equal(120.25, userProfile.score); + Assert.Equal([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 100], userProfile.avatar); + Assert.Equal("Hello! I'm a super \"duper\" user!", userProfile.bio); + Assert.Equal([(IPAddress.Parse("192.168.1.100"), DateTimeOffset.Parse("2024-03-20T14:30:00+00:00"))], userProfile.LastLogins); + } +} diff --git a/duper_uniffi/tests/csharp/GlobalUsings.cs b/duper_uniffi/tests/csharp/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/duper_uniffi/tests/csharp/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file -- 2.51.2