diff --git a/.config/CredScanSuppressions.json b/.config/CredScanSuppressions.json index 02e494e56ba75..534d18500bc51 100644 --- a/.config/CredScanSuppressions.json +++ b/.config/CredScanSuppressions.json @@ -7,7 +7,8 @@ "src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DSA/DSAKeyPemTests.cs", "src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/EC/ECKeyPemTests.cs", "src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/RSAKeyPemTests.cs", - "src/libraries/System.Security.Cryptography/tests/X509Certificates/TestData.cs" + "src/libraries/System.Security.Cryptography/tests/X509Certificates/TestData.cs", + "src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.key" ], "placeholder": [ "-----BEGIN PRIVATE KEY-----", diff --git a/eng/Versions.props b/eng/Versions.props index 940681731e6ff..59fad667242a9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -165,6 +165,14 @@ 2.0.4 4.12.0 2.14.3 + + 3.19.4 + 2.46.0 + 2.46.0 + 2.46.3 + 2.46.3 + 2.45.0 + 2.45.0 1.1.2-beta1.22403.2 diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 5a3bf10c3e00a..7c49eaeb6f3e1 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -30,6 +30,7 @@ $([MSBuild]::NormalizeDirectory('$(CoreCLRArtifactsPath)', 'build')) $([MSBuild]::NormalizeDirectory('$(MonoArtifactsPath)', 'cross', $(TargetOS.ToLowerInvariant())-$(TargetArchitecture.ToLowerInvariant()))) + $([MSBuild]::NormalizeDirectory('$(LibrariesArtifactsPath)', 'obj', 'grpcserver', 'docker')) $([MSBuild]::NormalizeDirectory('$(LibrariesArtifactsPath)', 'packages', '$(LibrariesConfiguration)')) $([MSBuild]::NormalizeDirectory('$(LibrariesPackagesDir)', 'Shipping')) diff --git a/eng/pipelines/common/platform-matrix.yml b/eng/pipelines/common/platform-matrix.yml index 4ae4b32db8a9f..9262b5d36a165 100644 --- a/eng/pipelines/common/platform-matrix.yml +++ b/eng/pipelines/common/platform-matrix.yml @@ -207,7 +207,7 @@ jobs: platform: Linux_bionic_arm64 shouldContinueOnError: ${{ parameters.shouldContinueOnError }} container: - image: ubuntu-18.04-android-20220628174908-5789942 + image: ubuntu-18.04-android-20220808192756-8fcaabc registry: mcr jobParameters: runtimeFlavor: mono @@ -236,7 +236,7 @@ jobs: platform: Linux_bionic_x64 shouldContinueOnError: ${{ parameters.shouldContinueOnError }} container: - image: ubuntu-18.04-android-20220628174908-5789942 + image: ubuntu-18.04-android-20220808192756-8fcaabc registry: mcr jobParameters: runtimeFlavor: mono @@ -494,7 +494,7 @@ jobs: platform: Android_x64 shouldContinueOnError: ${{ parameters.shouldContinueOnError }} container: - image: ubuntu-18.04-android-20220628174908-5789942 + image: ubuntu-18.04-android-20220808192756-8fcaabc registry: mcr jobParameters: runtimeFlavor: mono @@ -519,7 +519,7 @@ jobs: platform: Android_x86 shouldContinueOnError: ${{ parameters.shouldContinueOnError }} container: - image: ubuntu-18.04-android-20220628174908-5789942 + image: ubuntu-18.04-android-20220808192756-8fcaabc registry: mcr jobParameters: runtimeFlavor: mono @@ -544,7 +544,7 @@ jobs: platform: Android_arm shouldContinueOnError: ${{ parameters.shouldContinueOnError }} container: - image: ubuntu-18.04-android-20220628174908-5789942 + image: ubuntu-18.04-android-20220808192756-8fcaabc registry: mcr jobParameters: runtimeFlavor: mono @@ -569,7 +569,7 @@ jobs: platform: Android_arm64 shouldContinueOnError: ${{ parameters.shouldContinueOnError }} container: - image: ubuntu-18.04-android-20220628174908-5789942 + image: ubuntu-18.04-android-20220808192756-8fcaabc registry: mcr jobParameters: runtimeFlavor: mono diff --git a/eng/pipelines/runtime-android-grpc-client-tests.yml b/eng/pipelines/runtime-android-grpc-client-tests.yml new file mode 100644 index 0000000000000..5322771f3a963 --- /dev/null +++ b/eng/pipelines/runtime-android-grpc-client-tests.yml @@ -0,0 +1,45 @@ +# We run this pipeline on a schedule and also developers can run it +# via /azp run command on PRs. +# +# Setting batch to true, triggers one build at a time. +# if there is a push while a build in progress, it will wait, +# until the running build finishes, and produce a build with all the changes +# that happened during the last build. +trigger: none + +schedules: + - cron: "0 9,21 * * *" # run at 9:00 and 21:00 (UTC) which is 1:00 and 13:00 (PST). + displayName: grpc-dotnet Android client test schedule + branches: + include: + - main + always: true + +variables: + - template: /eng/pipelines/common/variables.yml + +jobs: + +# +# Android emulators +# Build the whole product using Mono and run libraries tests +# +- template: /eng/pipelines/common/platform-matrix.yml + parameters: + jobTemplate: /eng/pipelines/common/global-build-job.yml + helixQueuesTemplate: /eng/pipelines/libraries/helix-queues-setup.yml + buildConfig: Release + runtimeFlavor: mono + platforms: + - Android_x64 + jobParameters: + testGroup: innerloop + nameSuffix: AllSubsets_Mono_gRPC + buildArgs: -s mono+libs+host+packs+libs.tests -c $(_BuildConfig) /p:ArchiveTests=true /p:RunGrpcTestsOnly=true /p:BuildGrpcServerDockerImage=true + timeoutInMinutes: 180 + # extra steps, run tests + extraStepsTemplate: /eng/pipelines/libraries/helix.yml + extraStepsParameters: + creator: dotnet-bot + extraHelixArguments: /p:RunGrpcTestsOnly=true /p:BuildGrpcServerDockerImage=true + testRunNamePrefixSuffix: Mono_$(_BuildConfig) diff --git a/src/libraries/sendtohelixhelp.proj b/src/libraries/sendtohelixhelp.proj index 59cd95650c13e..f7fadf8a6323f 100644 --- a/src/libraries/sendtohelixhelp.proj +++ b/src/libraries/sendtohelixhelp.proj @@ -155,6 +155,14 @@ + + + + + + + + @@ -165,6 +173,7 @@ @(HelixPreCommand) + @(HelixPostCommand) $(HelixCommandPrefix) @(HelixCommandPrefixItem -> 'set "%(Identity)"', ' & ') $(HelixCommandPrefix) @(HelixCommandPrefixItem, ' ') true diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 8d1d39d2143dc..221bbde38aca1 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -195,6 +195,9 @@ + + + @@ -540,7 +543,8 @@ - + + @@ -560,9 +564,11 @@ + - + @@ -606,7 +612,7 @@ - + @@ -615,13 +621,13 @@ BuildInParallel="false" /> - + - + @@ -630,13 +636,13 @@ BuildInParallel="false" /> - + - + + + Exe + true + $(NetCoreAppCurrent) + Android.Device_Emulator.gRPC.Test.dll + 42 + + false + true + true + true + + true + true + + enable + enable + + + CS8981;SYSLIB0039 + + + + true + + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/Program.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/Program.cs new file mode 100644 index 0000000000000..21fda1ab72206 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/Program.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +// The code of the tests is cloned from https://github.com/grpc/grpc-dotnet +using Grpc.Shared.TestAssets; + +var skippedTests = new[] +{ + "compute_engine_creds", + "jwt_token_creds", + "oauth2_auth_token", + "per_rpc_creds", + "client_compressed_streaming", // flaky test +}; + +var configurations = new[] +{ + new ClientOptions + { + ServerHost = "10.0.2.2", + ServerPort = 50052, + UseTls = true, + }, +}; + +int failedTests = 0; + +foreach (var options in configurations) +{ + Console.WriteLine($""" + gRPC client options: + -------------------- + ClientType: {options.ClientType} + ServerHost: {options.ServerHost} + ServerHostOverride: {options.ServerHostOverride} + ServerPort: {options.ServerPort} + UseTls: {options.UseTls} + UseTestCa: {options.UseTestCa} + DefaultServiceAccount: {options.DefaultServiceAccount} + OAuthScope: {options.OAuthScope} + ServiceAccountKeyFile: {options.ServiceAccountKeyFile} + UseHttp3: {options.UseHttp3} + --- + """); + + foreach (var testName in InteropClient.TestNames) + { + if (skippedTests.Contains(testName)) + { + Log(testName, "SKIPPED"); + continue; + } + + options.TestCase = testName; + var client = new InteropClientWrapper(options); + + try + { + Log(testName, "STARTED"); + await client.Run(); + Log(testName, "PASSED"); + } catch (Exception e) { + Log(testName, "FAILED"); + Console.Error.WriteLine(e); + failedTests++; + } + } +} + +return 42 + failedTests; + +void Log(string testName, string status) + => Console.WriteLine($"TestCase: {testName} ... {status}"); + +sealed class InteropClientWrapper +{ + private readonly InteropClient _interopClient; + + [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Grpc.Testing.TestService.TestServiceClient", "Android.Device_Emulator.gRPC.Test")] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Grpc.Testing.UnimplementedService.UnimplementedServiceClient", "Android.Device_Emulator.gRPC.Test")] + public InteropClientWrapper(ClientOptions options) + { + _interopClient = new InteropClient(options); + } + + public Task Run() + => _interopClient.Run(); +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/README.md b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/README.md new file mode 100644 index 0000000000000..6ac6505242024 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/README.md @@ -0,0 +1,2 @@ +res - Android resource folder containing self-signed certificates and Android network configuration +dotnet-grpc - copied from https://github.com/grpc/grpc-dotnet diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/THIRD-PARTY-NOTICES b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/THIRD-PARTY-NOTICES new file mode 100644 index 0000000000000..e0c2c13933e21 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/THIRD-PARTY-NOTICES @@ -0,0 +1,26 @@ +.NET Runtime uses third-party libraries or other resources that may be +distributed under licenses different than the .NET Runtime software. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. Post an issue or email us: + + dotnet@microsoft.com + +The attached notices are provided for information only. + +License notice for Grpc.Net.Client +------------------------------- + +Copyright 2019 The gRPC Authors + +Licensed 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. diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/test/Shared/HttpEventSourceListener.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/test/Shared/HttpEventSourceListener.cs new file mode 100755 index 0000000000000..d03fe0dc2b750 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/test/Shared/HttpEventSourceListener.cs @@ -0,0 +1,120 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed 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. + +#endregion + +using System.Diagnostics.Tracing; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Grpc.Tests.Shared +{ + public sealed class HttpEventSourceListener : EventListener + { + private readonly StringBuilder _messageBuilder = new StringBuilder(); + private readonly ILogger? _logger; + private readonly object _lock = new object(); + private bool _disposed; + + public HttpEventSourceListener(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(nameof(HttpEventSourceListener)); + _logger.LogDebug($"Starting {nameof(HttpEventSourceListener)}."); + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + base.OnEventSourceCreated(eventSource); + + if (IsHttpEventSource(eventSource)) + { + lock (_lock) + { + if (!_disposed) + { + EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All); + } + } + } + } + + private static bool IsHttpEventSource(EventSource eventSource) + { + return eventSource.Name.Contains("System.Net.Quic") || eventSource.Name.Contains("System.Net.Http"); + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + base.OnEventWritten(eventData); + + if (!IsHttpEventSource(eventData.EventSource)) + { + return; + } + + string message; + lock (_messageBuilder) + { + _messageBuilder.Append("<- Event "); + _messageBuilder.Append(eventData.EventSource.Name); + _messageBuilder.Append(" - "); + _messageBuilder.Append(eventData.EventName); + _messageBuilder.Append(" : "); +#if !NET472 + _messageBuilder.AppendJoin(',', eventData.Payload!); +#else + _messageBuilder.Append(string.Join(",", eventData.Payload!.ToArray())); +#endif + _messageBuilder.Append(" ->"); + message = _messageBuilder.ToString(); + _messageBuilder.Clear(); + } + + // We don't know the state of the logger after dispose. + // Ensure that any messages written in the background aren't + // logged after the listener has been disposed in the test. + lock (_lock) + { + if (!_disposed) + { + // EventListener base constructor subscribes to events. + // It is possible to start getting events before the + // super constructor is run and logger is assigned. + _logger?.LogDebug(message); + } + } + } + + public override string ToString() + { + return _messageBuilder.ToString(); + } + + public override void Dispose() + { + base.Dispose(); + + lock (_lock) + { + if (!_disposed) + { + _logger?.LogDebug($"Stopping {nameof(HttpEventSourceListener)}."); + _disposed = true; + } + } + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/.gitignore b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/.gitignore new file mode 100644 index 0000000000000..434635979192b --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/.gitignore @@ -0,0 +1,5 @@ +# we need the server1.pfx file +!Certs/InteropTests/server1.pfx + +# we copy eng/Versions.props during the build process +InteropTestsWebsite/Versions.props diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/README.md b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/README.md new file mode 100644 index 0000000000000..18efda8276bb6 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/README.md @@ -0,0 +1,8 @@ +Keys taken from https://github.com/grpc/grpc/tree/master/src/core/tsi/test_creds +so that interop server in this project is compatible with interop clients +implemented in other gRPC languages. + +The server1.pem and server1.key were combined into server1.pfx. The password is PLACEHOLDER. These certs are not secure, do not use in production. +``` +openssl pkcs12 -export -out server1.pfx -inkey server1.key -in server1.pem -certfile ca.pem +``` diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/ca.pem b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/ca.pem new file mode 100644 index 0000000000000..49d39cd8ed5f8 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw +MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01 +diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO +Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k +QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c +qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV +LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud +DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a +THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S +CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9 +/OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt +bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw +eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw== +-----END CERTIFICATE----- diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.key b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.key new file mode 100644 index 0000000000000..086462992cfbe --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnE443EknxvxBq +6+hvn/t09hl8hx366EBYvZmVM/NC+7igXRAjiJiA/mIaCvL3MS0Iz5hBLxSGICU+ +WproA3GCIFITIwcf/ETyWj/5xpgZ4AKrLrjQmmX8mhwUajfF3UvwMJrCOVqPp67t +PtP+2kBXaqrXdvnvXR41FsIB8V7zIAuIZB6bHQhiGVlc1sgZYsE2EGG9WMmHtS86 +qkAOTjG2XyjmPTGAwhGDpYkYrpzp99IiDh4/Veai81hn0ssQkbry0XRD/Ig3jcHh +23WiriPNJ0JsbgXUSLKRPZObA9VgOLy2aXoN84IMaeK3yy+cwSYG/99w93fUZJte +MXwz4oYZAgMBAAECggEBAIVn2Ncai+4xbH0OLWckabwgyJ4IM9rDc0LIU368O1kU +koais8qP9dujAWgfoh3sGh/YGgKn96VnsZjKHlyMgF+r4TaDJn3k2rlAOWcurGlj +1qaVlsV4HiEzp7pxiDmHhWvp4672Bb6iBG+bsjCUOEk/n9o9KhZzIBluRhtxCmw5 +nw4Do7z00PTvN81260uPWSc04IrytvZUiAIx/5qxD72bij2xJ8t/I9GI8g4FtoVB +8pB6S/hJX1PZhh9VlU6Yk+TOfOVnbebG4W5138LkB835eqk3Zz0qsbc2euoi8Hxi +y1VGwQEmMQ63jXz4c6g+X55ifvUK9Jpn5E8pq+pMd7ECgYEA93lYq+Cr54K4ey5t +sWMa+ye5RqxjzgXj2Kqr55jb54VWG7wp2iGbg8FMlkQwzTJwebzDyCSatguEZLuB +gRGroRnsUOy9vBvhKPOch9bfKIl6qOgzMJB267fBVWx5ybnRbWN/I7RvMQf3k+9y +biCIVnxDLEEYyx7z85/5qxsXg/MCgYEA7wmWKtCTn032Hy9P8OL49T0X6Z8FlkDC +Rk42ygrc/MUbugq9RGUxcCxoImOG9JXUpEtUe31YDm2j+/nbvrjl6/bP2qWs0V7l +dTJl6dABP51pCw8+l4cWgBBX08Lkeen812AAFNrjmDCjX6rHjWHLJcpS18fnRRkP +V1d/AHWX7MMCgYEA6Gsw2guhp0Zf2GCcaNK5DlQab8OL4Hwrpttzo4kuTlwtqNKp +Q9H4al9qfF4Cr1TFya98+EVYf8yFRM3NLNjZpe3gwYf2EerlJj7VLcahw0KKzoN1 +QBENfwgPLRk5sDkx9VhSmcfl/diLroZdpAwtv3vo4nEoxeuGFbKTGx3Qkf0CgYEA +xyR+dcb05Ygm3w4klHQTowQ10s1H80iaUcZBgQuR1ghEtDbUPZHsoR5t1xCB02ys +DgAwLv1bChIvxvH/L6KM8ovZ2LekBX4AviWxoBxJnfz/EVau98B0b1auRN6eSC83 +FRuGldlSOW1z/nSh8ViizSYE5H5HX1qkXEippvFRE88CgYB3Bfu3YQY60ITWIShv +nNkdcbTT9eoP9suaRJjw92Ln+7ZpALYlQMKUZmJ/5uBmLs4RFwUTQruLOPL4yLTH +awADWUzs3IRr1fwn9E+zM8JVyKCnUEM3w4N5UZskGO2klashAd30hWO+knRv/y0r +uGIYs9Ek7YXlXIRVrzMwcsrt1w== +-----END PRIVATE KEY----- diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.pem b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.pem new file mode 100644 index 0000000000000..88244f856c622 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtDCCApygAwIBAgIUbJfTREJ6k6/+oInWhV1O1j3ZT0IwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw +MDMxODAzMTA0MloXDTMwMDMxNjAzMTA0MlowZTELMAkGA1UEBhMCVVMxETAPBgNV +BAgMCElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRUwEwYDVQQKDAxFeGFtcGxl +LCBDby4xGjAYBgNVBAMMESoudGVzdC5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5xOONxJJ8b8Qauvob5/7dPYZfIcd+uhAWL2ZlTPz +Qvu4oF0QI4iYgP5iGgry9zEtCM+YQS8UhiAlPlqa6ANxgiBSEyMHH/xE8lo/+caY +GeACqy640Jpl/JocFGo3xd1L8DCawjlaj6eu7T7T/tpAV2qq13b5710eNRbCAfFe +8yALiGQemx0IYhlZXNbIGWLBNhBhvVjJh7UvOqpADk4xtl8o5j0xgMIRg6WJGK6c +6ffSIg4eP1XmovNYZ9LLEJG68tF0Q/yIN43B4dt1oq4jzSdCbG4F1EiykT2TmwPV +YDi8tml6DfOCDGnit8svnMEmBv/fcPd31GSbXjF8M+KGGQIDAQABo2swaTAJBgNV +HRMEAjAAMAsGA1UdDwQEAwIF4DBPBgNVHREESDBGghAqLnRlc3QuZ29vZ2xlLmZy +ghh3YXRlcnpvb2kudGVzdC5nb29nbGUuYmWCEioudGVzdC55b3V0dWJlLmNvbYcE +wKgBAzANBgkqhkiG9w0BAQsFAAOCAQEAS8hDQA8PSgipgAml7Q3/djwQ644ghWQv +C2Kb+r30RCY1EyKNhnQnIIh/OUbBZvh0M0iYsy6xqXgfDhCB93AA6j0i5cS8fkhH +Jl4RK0tSkGQ3YNY4NzXwQP/vmUgfkw8VBAZ4Y4GKxppdATjffIW+srbAmdDruIRM +wPeikgOoRrXf0LA1fi4TqxARzeRwenQpayNfGHTvVF9aJkl8HoaMunTAdG5pIVcr +9GKi/gEMpXUJbbVv3U5frX1Wo4CFo+rZWJ/LyCMeb0jciNLxSdMwj/E/ZuExlyeZ +gc9ctPjSMvgSyXEKv6Vwobleeg88V2ZgzenziORoWj4KszG/lbQZvg== +-----END CERTIFICATE----- diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.pfx b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.pfx new file mode 100644 index 0000000000000..182e5ecfe7d1c Binary files /dev/null and b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Certs/InteropTests/server1.pfx differ diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Dockerfile b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Dockerfile new file mode 100644 index 0000000000000..a3e5798c601ce --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env +WORKDIR /app + +# Copy everything +COPY . ./ +WORKDIR /app/InteropTestsWebsite +RUN dotnet --info +RUN dotnet restore +RUN dotnet publish --framework net6.0 -c Release -o out -p:LatestFramework=true + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app +COPY --from=build-env /app/InteropTestsWebsite/out . +ENTRYPOINT ["dotnet", "InteropTestsWebsite.dll", "--use_tls", "true"] diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/InteropTestsWebsite.csproj b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/InteropTestsWebsite.csproj new file mode 100644 index 0000000000000..0c763a85c8c88 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/InteropTestsWebsite.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + InProcess + false + enable + + + + CS8981;SYSLIB0039 + + + + + + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/Program.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/Program.cs new file mode 100644 index 0000000000000..a56fa355fbf02 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/Program.cs @@ -0,0 +1,79 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed 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. + +#endregion + +using Microsoft.AspNetCore.Server.Kestrel.Core; + +namespace InteropTestsWebsite +{ + public class Program + { + private const LogLevel MinimumLogLevel = LogLevel.Debug; + + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddLogging(builder => builder.SetMinimumLevel(MinimumLogLevel)); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel((context, options) => + { + // Support --port and --use_tls cmdline arguments normally supported + // by gRPC interop servers. + var http2Port = context.Configuration.GetValue("port", 50052); + var http1Port = context.Configuration.GetValue("port_http1", -1); + var http3Port = context.Configuration.GetValue("port_http3", -1); + var useTls = context.Configuration.GetValue("use_tls", false); + + options.Limits.MinRequestBodyDataRate = null; + options.ListenAnyIP(http2Port, o => ConfigureEndpoint(o, useTls, HttpProtocols.Http2)); + if (http1Port != -1) + { + options.ListenAnyIP(http1Port, o => ConfigureEndpoint(o, useTls, HttpProtocols.Http1)); + } + if (http3Port != -1) + { +#pragma warning disable CA2252 // This API requires opting into preview features + options.ListenAnyIP(http3Port, o => ConfigureEndpoint(o, useTls, HttpProtocols.Http3)); +#pragma warning restore CA2252 // This API requires opting into preview features + } + + void ConfigureEndpoint(ListenOptions listenOptions, bool useTls, HttpProtocols httpProtocols) + { + Console.WriteLine($"Enabling connection encryption: {useTls}"); + + if (useTls) + { + var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location); + var certPath = Path.Combine(basePath!, "Certs", "server1.pfx"); + + listenOptions.UseHttps(certPath, "PLACEHOLDER"); + } + listenOptions.Protocols = httpProtocols; + } + }); + webBuilder.UseStartup(); + }); + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/README.md b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/README.md new file mode 100644 index 0000000000000..91c7085da85e2 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/README.md @@ -0,0 +1,27 @@ +Running Grpc.Core interop client against Grpc.AspNetCore.Server interop server. +Context: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md + +## Start the InteropTestsWebsite + +``` +# From this directory +$ dotnet run +Now listening on: http://localhost:50052 +``` + +## Build gRPC C# as a developer: +Follow https://github.com/grpc/grpc/tree/master/src/csharp +``` +python tools/run_tests/run_tests.py -l csharp -c dbg --build_only +``` + +## Running the interop client + +``` +cd src/csharp/Grpc.IntegrationTesting.Client/bin/Debug/net45 + +mono Grpc.IntegrationTesting.Client.exe --server_host=localhost --server_port=50052 --test_case=large_unary +``` + +NOTE: Currently the some tests will fail because not all the features are implemented +by Grpc.AspNetCore.Server diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/Startup.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/Startup.cs new file mode 100644 index 0000000000000..72cb5274edbe7 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/Startup.cs @@ -0,0 +1,54 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed 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. + +#endregion + +using Grpc.Testing; + +namespace InteropTestsWebsite +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); + services.AddCors(o => + { + o.AddPolicy("InteropTests", builder => + { + builder.AllowAnyOrigin(); + builder.AllowAnyMethod(); + builder.AllowAnyHeader(); + builder.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding", "x-grpc-test-echo-initial", "x-grpc-test-echo-trailing-bin"); + }); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseCors(); + app.UseGrpcWeb(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService().RequireCors("InteropTests").EnableGrpcWeb(); + }); + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/TestServiceImpl.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/TestServiceImpl.cs new file mode 100644 index 0000000000000..3f5cda2357efa --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/InteropTestsWebsite/TestServiceImpl.cs @@ -0,0 +1,149 @@ +#region Copyright notice and license + +// Copyright 2015-2016 gRPC authors. +// +// Licensed 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. + +#endregion + +using Google.Protobuf; +using Grpc.Core; +using Grpc.Shared.TestAssets; + +namespace Grpc.Testing +{ + // Implementation copied from https://github.com/grpc/grpc/blob/master/src/csharp/Grpc.IntegrationTesting/TestServiceImpl.cs + public class TestServiceImpl : TestService.TestServiceBase + { + public override Task EmptyCall(Empty request, ServerCallContext context) + { + return Task.FromResult(new Empty()); + } + + public override async Task UnaryCall(SimpleRequest request, ServerCallContext context) + { + await EnsureEchoMetadataAsync(context, request.ResponseCompressed?.Value ?? false); + EnsureEchoStatus(request.ResponseStatus, context); + EnsureCompression(request.ExpectCompressed, context); + + var response = new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) }; + return response; + } + + public override async Task StreamingOutputCall(StreamingOutputCallRequest request, IServerStreamWriter responseStream, ServerCallContext context) + { + await EnsureEchoMetadataAsync(context, request.ResponseParameters.Any(rp => rp.Compressed?.Value ?? false)); + EnsureEchoStatus(request.ResponseStatus, context); + + foreach (var responseParam in request.ResponseParameters) + { + responseStream.WriteOptions = !(responseParam.Compressed?.Value ?? false) + ? new WriteOptions(WriteFlags.NoCompress) + : null; + + var response = new StreamingOutputCallResponse { Payload = CreateZerosPayload(responseParam.Size) }; + await responseStream.WriteAsync(response); + } + } + + public override async Task StreamingInputCall(IAsyncStreamReader requestStream, ServerCallContext context) + { + await EnsureEchoMetadataAsync(context); + + int sum = 0; + await requestStream.ForEachAsync(request => + { + EnsureCompression(request.ExpectCompressed, context); + + sum += request.Payload.Body.Length; + return Task.CompletedTask; + }); + + return new StreamingInputCallResponse { AggregatedPayloadSize = sum }; + } + + public override async Task FullDuplexCall(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + await EnsureEchoMetadataAsync(context); + + await requestStream.ForEachAsync(async request => + { + EnsureEchoStatus(request.ResponseStatus, context); + foreach (var responseParam in request.ResponseParameters) + { + var response = new StreamingOutputCallResponse { Payload = CreateZerosPayload(responseParam.Size) }; + await responseStream.WriteAsync(response); + } + }); + } + + public override Task HalfDuplexCall(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + throw new NotImplementedException(); + } + + private static Payload CreateZerosPayload(int size) + { + return new Payload { Body = ByteString.CopyFrom(new byte[size]) }; + } + + private static async Task EnsureEchoMetadataAsync(ServerCallContext context, bool enableCompression = false) + { + var echoInitialList = context.RequestHeaders.Where((entry) => entry.Key == "x-grpc-test-echo-initial").ToList(); + + // Append grpc internal compression header if compression is requested by the client + if (enableCompression) + { + echoInitialList.Add(new Metadata.Entry("grpc-internal-encoding-request", "gzip")); + } + + if (echoInitialList.Any()) + { + var entry = echoInitialList.Single(); + await context.WriteResponseHeadersAsync(new Metadata { entry }); + } + + var echoTrailingList = context.RequestHeaders.Where((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ToList(); + if (echoTrailingList.Any()) + { + context.ResponseTrailers.Add(echoTrailingList.Single()); + } + } + + private static void EnsureEchoStatus(EchoStatus responseStatus, ServerCallContext context) + { + if (responseStatus != null) + { + var statusCode = (StatusCode)responseStatus.Code; + context.Status = new Status(statusCode, responseStatus.Message); + } + } + + private static void EnsureCompression(BoolValue expectCompressed, ServerCallContext context) + { + if (expectCompressed != null) + { + // ServerCallContext.RequestHeaders filters out grpc-* headers + // Get grpc-encoding from HttpContext instead + var encoding = context.GetHttpContext().Request.Headers.SingleOrDefault(h => string.Equals(h.Key, "grpc-encoding", StringComparison.OrdinalIgnoreCase)).Value.SingleOrDefault(); + if (expectCompressed.Value) + { + if (encoding == null || encoding == "identity") + { + throw new RpcException(new Status(StatusCode.InvalidArgument, string.Empty)); + } + } + } + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/empty.proto b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/empty.proto new file mode 100644 index 0000000000000..6a0aa88dfde13 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/empty.proto @@ -0,0 +1,28 @@ + +// Copyright 2015 gRPC authors. +// +// Licensed 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. + +syntax = "proto3"; + +package grpc.testing; + +// An empty message that you can re-use to avoid defining duplicated empty +// messages in your project. A typical example is to use it as argument or the +// return value of a service API. For instance: +// +// service Foo { +// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; +// }; +// +message Empty {} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/messages.proto b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/messages.proto new file mode 100644 index 0000000000000..7b1b7286dced1 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/messages.proto @@ -0,0 +1,165 @@ + +// Copyright 2015-2016 gRPC authors. +// +// Licensed 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. + +// Message definitions to be used by integration test service definitions. + +syntax = "proto3"; + +package grpc.testing; + +// TODO(dgq): Go back to using well-known types once +// https://github.com/grpc/grpc/issues/6980 has been fixed. +// import "google/protobuf/wrappers.proto"; +message BoolValue { + // The bool value. + bool value = 1; +} + +// The type of payload that should be returned. +enum PayloadType { + // Compressable text format. + COMPRESSABLE = 0; +} + +// A block of data, to simply increase gRPC message size. +message Payload { + // The type of data in body. + PayloadType type = 1; + // Primary contents of payload. + bytes body = 2; +} + +// A protobuf representation for grpc status. This is used by test +// clients to specify a status that the server should attempt to return. +message EchoStatus { + int32 code = 1; + string message = 2; +} + +// Unary request. +message SimpleRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + PayloadType response_type = 1; + + // Desired payload size in the response from the server. + int32 response_size = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether SimpleResponse should include username. + bool fill_username = 4; + + // Whether SimpleResponse should include OAuth scope. + bool fill_oauth_scope = 5; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue response_compressed = 6; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // Whether the server should expect this request to be compressed. + BoolValue expect_compressed = 8; +} + +// Unary response, as configured by the request. +message SimpleResponse { + // Payload to increase message size. + Payload payload = 1; + // The user the request came from, for verifying authentication was + // successful when the client expected it. + string username = 2; + // OAuth scope. + string oauth_scope = 3; +} + +// Client-streaming request. +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + Payload payload = 1; + + // Whether the server should expect this request to be compressed. This field + // is "nullable" in order to interoperate seamlessly with servers not able to + // implement the full compression tests by introspecting the call to verify + // the request's compression status. + BoolValue expect_compressed = 2; + + // Not expecting any payload from the response. +} + +// Client-streaming response. +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + int32 aggregated_payload_size = 1; +} + +// Configuration for a particular response. +message ResponseParameters { + // Desired payload sizes in responses from the server. + int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + int32 interval_us = 2; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue compressed = 3; +} + +// Server-streaming request. +message StreamingOutputCallRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + PayloadType response_type = 1; + + // Configuration for each expected response message. + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether server should return a given status + EchoStatus response_status = 7; +} + +// Server-streaming response, as configured by the request and parameters. +message StreamingOutputCallResponse { + // Payload to increase response size. + Payload payload = 1; +} + +// For reconnect interop test only. +// Client tells server what reconnection parameters it used. +message ReconnectParams { + int32 max_reconnect_backoff_ms = 1; +} + +// For reconnect interop test only. +// Server tells client whether its reconnects are following the spec and the +// reconnect backoffs it saw. +message ReconnectInfo { + bool passed = 1; + repeated int32 backoff_ms = 2; +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/test.proto b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/test.proto new file mode 100644 index 0000000000000..86d6ab60506a4 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Proto/grpc/testing/test.proto @@ -0,0 +1,79 @@ + +// Copyright 2015-2016 gRPC authors. +// +// Licensed 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +syntax = "proto3"; + +import "empty.proto"; +import "messages.proto"; + +package grpc.testing; + +// A simple service to test the various types of RPCs and experiment with +// performance with various types of payload. +service TestService { + // One empty request followed by one empty response. + rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); + + // One request followed by one response. + rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by one response. Response has cache control + // headers set such that a caching HTTP proxy (such as GFE) can + // satisfy subsequent requests. + rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by a sequence of responses (streamed download). + // The server returns the payload with client desired type and sizes. + rpc StreamingOutputCall(StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by one response (streamed upload). + // The server returns the aggregated size of client payload as the result. + rpc StreamingInputCall(stream StreamingInputCallRequest) + returns (StreamingInputCallResponse); + + // A sequence of requests with each request served by the server immediately. + // As one request could lead to multiple responses, this interface + // demonstrates the idea of full duplexing. + rpc FullDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by a sequence of responses. + // The server buffers all the client requests and then serves them in order. A + // stream of responses are returned to the client when the server starts with + // first request. + rpc HalfDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // The test server will not implement this method. It will be used + // to test the behavior when clients call unimplemented methods. + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A simple service NOT implemented at servers so clients can test for +// that case. +service UnimplementedService { + // A call that no server should implement + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A service used to control reconnect server. +service ReconnectService { + rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); + rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/Assert.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/Assert.cs new file mode 100644 index 0000000000000..7f15a7f962f95 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/Assert.cs @@ -0,0 +1,129 @@ +#region Copyright notice and license + +// Copyright 2015-2016 gRPC authors. +// +// Licensed 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. + +#endregion + +using System.Collections; + +namespace Grpc.Shared.TestAssets +{ + internal static class Assert + { + public static void IsTrue(bool condition) + { + if (!condition) + { + throw new InvalidOperationException("Expected true but got false."); + } + } + + public static void IsFalse(bool condition) + { + if (condition) + { + throw new InvalidOperationException("Expected false but got true."); + } + } + + public static void AreEqual(object expected, object actual) + { + if (!Equals(expected, actual)) + { + throw new InvalidOperationException($"Expected {expected} but got {actual}."); + } + } + + public static void IsNotNull(object value) + { + if (value == null) + { + throw new InvalidOperationException("Expected not null but got null."); + } + } + + public static void Fail() + { + throw new InvalidOperationException("Failure assert."); + } + + public static async Task ThrowsAsync(Func action) where TException : Exception + { + try + { + await action(); + } + catch (Exception ex) + { + if (ex.GetType() == typeof(TException)) + { + return (TException)ex; + } + + throw new InvalidOperationException($"Expected ${typeof(TException)} but got ${ex.GetType()}."); + } + + throw new InvalidOperationException("No exception thrown."); + } + + public static TException Throws(Action action) where TException : Exception + { + try + { + action(); + } + catch (Exception ex) + { + if (ex.GetType() == typeof(TException)) + { + return (TException)ex; + } + + throw new InvalidOperationException($"Expected ${typeof(TException)} but got ${ex.GetType()}."); + } + + throw new InvalidOperationException("No exception thrown."); + } + + public static void Contains(object expected, ICollection actual) + { + foreach (var item in actual) + { + if (Equals(item, expected)) + { + return; + } + } + + throw new InvalidOperationException($"Could not find {expected} in the collection."); + } + } + + internal static class CollectionAssert + { + public static void AreEqual(IList expected, IList actual) + { + if (expected.Count != actual.Count) + { + throw new InvalidOperationException($"Collection lengths differ. {expected.Count} but got {actual.Count}."); + } + + for (var i = 0; i < expected.Count; i++) + { + Assert.AreEqual(expected[i]!, actual[i]!); + } + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/AsyncStreamExtensions.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/AsyncStreamExtensions.cs new file mode 100644 index 0000000000000..c129d45ed81c8 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/AsyncStreamExtensions.cs @@ -0,0 +1,81 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed 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. + +#endregion + +using Grpc.Core; + +namespace Grpc.Shared.TestAssets +{ + // Implementation copied from https://github.com/grpc/grpc/blob/master/src/csharp/Grpc.Core/Utils/AsyncStreamExtensions.cs + internal static class AsyncStreamExtensions + { + /// + /// Reads the entire stream and executes an async action for each element. + /// + public static async Task ForEachAsync(this IAsyncStreamReader streamReader, Func asyncAction) + where T : class + { + while (await streamReader.MoveNext().ConfigureAwait(false)) + { + await asyncAction(streamReader.Current).ConfigureAwait(false); + } + } + + /// + /// Reads the entire stream and creates a list containing all the elements read. + /// + public static async Task> ToListAsync(this IAsyncStreamReader streamReader) + where T : class + { + var result = new List(); + while (await streamReader.MoveNext().ConfigureAwait(false)) + { + result.Add(streamReader.Current); + } + return result; + } + + /// + /// Writes all elements from given enumerable to the stream. + /// Completes the stream afterwards unless close = false. + /// + public static async Task WriteAllAsync(this IClientStreamWriter streamWriter, IEnumerable elements, bool complete = true) + where T : class + { + foreach (var element in elements) + { + await streamWriter.WriteAsync(element).ConfigureAwait(false); + } + if (complete) + { + await streamWriter.CompleteAsync().ConfigureAwait(false); + } + } + + /// + /// Writes all elements from given enumerable to the stream. + /// + public static async Task WriteAllAsync(this IServerStreamWriter streamWriter, IEnumerable elements) + where T : class + { + foreach (var element in elements) + { + await streamWriter.WriteAsync(element).ConfigureAwait(false); + } + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/ExceptionAssert.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/ExceptionAssert.cs new file mode 100644 index 0000000000000..7321b62df0871 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/ExceptionAssert.cs @@ -0,0 +1,54 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed 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. + +#endregion + +namespace Grpc.Shared.TestAssets +{ + public static class ExceptionAssert + { + public static async Task ThrowsAsync(Func action, params string[] possibleMessages) + where TException : Exception + { + try + { + await action(); + } + catch (TException ex) + { + if (possibleMessages == null || possibleMessages.Length == 0) + { + return ex; + } + foreach (string possibleMessage in possibleMessages) + { + if (Assert.Equals(possibleMessage, ex.Message)) + { + return ex; + } + } + + throw new Exception("Unexpected exception message." + Environment.NewLine + "Expected one of: " + string.Join(Environment.NewLine, possibleMessages) + Environment.NewLine + "Got: " + ex.Message + Environment.NewLine + Environment.NewLine + ex); + } + catch (Exception ex) + { + throw new Exception($"Exception of type {typeof(TException).Name} expected; got exception of type {ex.GetType().Name}.", ex); + } + + throw new Exception($"Exception of type {typeof(TException).Name} expected. No exception thrown."); + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/IChannelWrapper.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/IChannelWrapper.cs new file mode 100644 index 0000000000000..b14bee133db0d --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/IChannelWrapper.cs @@ -0,0 +1,44 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed 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. + +#endregion + +using Grpc.Core; +using Grpc.Net.Client; + +namespace Grpc.Shared.TestAssets +{ + public interface IChannelWrapper + { + ChannelBase Channel { get; } + Task ShutdownAsync(); + } + + public class GrpcChannelWrapper : IChannelWrapper + { + public ChannelBase Channel { get; } + + public GrpcChannelWrapper(GrpcChannel channel) + { + Channel = channel; + } + + public Task ShutdownAsync() + { + return Task.CompletedTask; + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/InteropClient.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/InteropClient.cs new file mode 100644 index 0000000000000..12549f8515bdf --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/InteropClient.cs @@ -0,0 +1,655 @@ +#region Copyright notice and license + +// Copyright 2015-2016 gRPC authors. +// +// Licensed 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. + +#endregion + +using System.Security.Cryptography.X509Certificates; +using Google.Protobuf; +using Grpc.Core; +using Grpc.Net.Client; +using Grpc.Testing; +using Empty = Grpc.Testing.Empty; +using System.Security.Authentication; + +namespace Grpc.Shared.TestAssets +{ + public class ClientOptions + { + public string? ClientType { get; set; } + public string? ServerHost { get; set; } + public string? ServerHostOverride { get; set; } + public int ServerPort { get; set; } + public string? TestCase { get; set; } + public bool UseTls { get; set; } + public bool UseTestCa { get; set; } + public string? DefaultServiceAccount { get; set; } + public string? OAuthScope { get; set; } + public string? ServiceAccountKeyFile { get; set; } + public bool UseHttp3 { get; set; } + } + + public class InteropClient + { + internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request"; + + private readonly ClientOptions options; + + public InteropClient(ClientOptions options) + { + this.options = options; + } + + public async Task Run() + { + var channel = HttpClientCreateChannel(); + + var message = "Running " + options.TestCase; + await RunTestCaseAsync(channel, options); + + await channel.ShutdownAsync(); + } + + private IChannelWrapper HttpClientCreateChannel() + { + var credentials = CreateCredentials(useTestCaOverride: false); + + string scheme; + if (!options.UseTls) + { + scheme = "http"; + } + else + { + scheme = "https"; + } + + HttpMessageHandler httpMessageHandler = CreateHttpClientHandler(); + if (options.UseHttp3) + { +#if NET6_0_OR_GREATER + httpMessageHandler = new Http3DelegatingHandler(httpMessageHandler); +#else + throw new Exception("HTTP/3 requires .NET 6 or later."); +#endif + } + + var channel = GrpcChannel.ForAddress($"{scheme}://{options.ServerHost}:{options.ServerPort}", new GrpcChannelOptions + { + Credentials = credentials, + HttpHandler = httpMessageHandler, + }); + + return new GrpcChannelWrapper(channel); + } + +#if NET6_0_OR_GREATER + private class Http3DelegatingHandler : DelegatingHandler + { + private static readonly Version Http3Version = new Version(3, 0); + + public Http3DelegatingHandler(HttpMessageHandler innerHandler) + { + InnerHandler = innerHandler; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Version = Http3Version; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + return base.SendAsync(request, cancellationToken); + } + } +#endif + + private HttpClientHandler CreateHttpClientHandler() + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (request, cert, chain, sslPolicyErrors) => true + }; + } + + private ChannelCredentials CreateCredentials(bool? useTestCaOverride = null) + => options.UseTls ? new SslCredentials() : ChannelCredentials.Insecure; + + private bool IsHttpClient() => string.Equals(options.ClientType, "httpclient", StringComparison.OrdinalIgnoreCase); + + private static TClient CreateClient(IChannelWrapper channel) where TClient : ClientBase + { + return (TClient)Activator.CreateInstance(typeof(TClient), channel.Channel)!; + } + + public static IEnumerable TestNames => Tests.Keys; + + private static readonly Dictionary> Tests = new Dictionary> + { + ["empty_unary"] = RunEmptyUnary, + ["large_unary"] = RunLargeUnary, + ["client_streaming"] = RunClientStreamingAsync, + ["server_streaming"] = RunServerStreamingAsync, + ["ping_pong"] = RunPingPongAsync, + ["empty_stream"] = RunEmptyStreamAsync, + ["compute_engine_creds"] = RunComputeEngineCreds, + ["cancel_after_begin"] = RunCancelAfterBeginAsync, + ["cancel_after_first_response"] = RunCancelAfterFirstResponseAsync, + ["timeout_on_sleeping_server"] = RunTimeoutOnSleepingServerAsync, + ["custom_metadata"] = RunCustomMetadataAsync, + ["status_code_and_message"] = RunStatusCodeAndMessageAsync, + ["special_status_message"] = RunSpecialStatusMessageAsync, + ["unimplemented_service"] = RunUnimplementedService, + ["unimplemented_method"] = RunUnimplementedMethod, + ["client_compressed_unary"] = RunClientCompressedUnary, + ["client_compressed_streaming"] = RunClientCompressedStreamingAsync, + ["server_compressed_unary"] = RunServerCompressedUnary, + ["server_compressed_streaming"] = RunServerCompressedStreamingAsync + }; + + private async Task RunTestCaseAsync(IChannelWrapper channel, ClientOptions options) + { + if (!Tests.TryGetValue(options.TestCase!, out var test)) + { + throw new ArgumentException("Unknown test case " + options.TestCase); + } + + await test(channel, options); + } + + public static async Task RunEmptyUnary(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var response = await client.EmptyCallAsync(new Empty()); + Assert.IsNotNull(response); + } + + public static async Task RunLargeUnary(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response = await client.UnaryCallAsync(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + } + + public static async Task RunClientStreamingAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var bodySizes = new List { 27182, 8, 1828, 45904 }.Select((size) => new StreamingInputCallRequest { Payload = CreateZerosPayload(size) }); + + using (var call = client.StreamingInputCall()) + { + await call.RequestStream.WriteAllAsync(bodySizes); + + var response = await call.ResponseAsync; + Assert.AreEqual(74922, response.AggregatedPayloadSize); + } + } + + public static async Task RunServerStreamingAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var bodySizes = new List { 31415, 9, 2653, 58979 }; + + var request = new StreamingOutputCallRequest + { + ResponseParameters = { bodySizes.Select((size) => new ResponseParameters { Size = size }) } + }; + + using (var call = client.StreamingOutputCall(request)) + { + var responseList = await call.ResponseStream.ToListAsync(); + CollectionAssert.AreEqual(bodySizes, responseList.Select((item) => item.Payload.Body.Length).ToList()); + } + } + + public static async Task RunPingPongAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + using (var call = client.FullDuplexCall()) + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 9 } }, + Payload = CreateZerosPayload(8) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(9, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 2653 } }, + Payload = CreateZerosPayload(1828) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(2653, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 58979 } }, + Payload = CreateZerosPayload(45904) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(58979, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.CompleteAsync(); + + Assert.IsFalse(await call.ResponseStream.MoveNext()); + } + } + + public static async Task RunEmptyStreamAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + using (var call = client.FullDuplexCall()) + { + await call.RequestStream.CompleteAsync(); + + var responseList = await call.ResponseStream.ToListAsync(); + Assert.AreEqual(0, responseList.Count); + } + } + + public static async Task RunComputeEngineCreds(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + var defaultServiceAccount = options.DefaultServiceAccount!; + var oauthScope = options.OAuthScope!; + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + FillUsername = true, + FillOauthScope = true + }; + + // not setting credentials here because they were set on channel already + var response = await client.UnaryCallAsync(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.IsFalse(string.IsNullOrEmpty(response.OauthScope)); + Assert.IsTrue(oauthScope.Contains(response.OauthScope)); + Assert.AreEqual(defaultServiceAccount, response.Username); + } + + public static async Task RunCancelAfterBeginAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var cts = new CancellationTokenSource(); + using (var call = client.StreamingInputCall(cancellationToken: cts.Token)) + { + // TODO(jtattermusch): we need this to ensure call has been initiated once we cancel it. + await Task.Delay(1000); + cts.Cancel(); + + var ex = await Assert.ThrowsAsync(() => call.ResponseAsync); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + } + + public static async Task RunCancelAfterFirstResponseAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var cts = new CancellationTokenSource(); + using (var call = client.FullDuplexCall(cancellationToken: cts.Token)) + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length); + + cts.Cancel(); + + try + { + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await call.ResponseStream.MoveNext(); + Assert.Fail(); + } + catch (RpcException ex) + { + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + } + } + + public static async Task RunTimeoutOnSleepingServerAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var deadline = DateTime.UtcNow.AddMilliseconds(1); + using (var call = client.FullDuplexCall(deadline: deadline)) + { + try + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest { Payload = CreateZerosPayload(27182) }); + } + catch (InvalidOperationException) + { + // Deadline was reached before write has started. Eat the exception and continue. + } + catch (RpcException) + { + // Deadline was reached before write has started. Eat the exception and continue. + } + + try + { + await call.ResponseStream.MoveNext(); + Assert.Fail(); + } + catch (RpcException ex) + { + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.StatusCode); + } + } + } + + public static async Task RunCustomMetadataAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + { + // step 1: test unary call + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + + var call = client.UnaryCallAsync(request, headers: CreateTestMetadata()); + await call.ResponseAsync; + + var responseHeaders = await call.ResponseHeadersAsync; + var responseTrailers = call.GetTrailers(); + + Assert.AreEqual("test_initial_metadata_value", responseHeaders.GetValue("x-grpc-test-echo-initial")!); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.GetValueBytes("x-grpc-test-echo-trailing-bin")!); + } + + { + // step 2: test full duplex call + var request = new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }; + + var call = client.FullDuplexCall(headers: CreateTestMetadata()); + + await call.RequestStream.WriteAsync(request); + await call.RequestStream.CompleteAsync(); + await call.ResponseStream.ToListAsync(); + + var responseHeaders = await call.ResponseHeadersAsync; + var responseTrailers = call.GetTrailers(); + + Assert.AreEqual("test_initial_metadata_value", responseHeaders.GetValue("x-grpc-test-echo-initial")!); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.GetValueBytes("x-grpc-test-echo-trailing-bin")!); + } + } + + public static async Task RunStatusCodeAndMessageAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var echoStatus = new EchoStatus + { + Code = 2, + Message = "test status message" + }; + + { + // step 1: test unary call + var request = new SimpleRequest { ResponseStatus = echoStatus }; + + var e = await ExceptionAssert.ThrowsAsync(async () => await client.UnaryCallAsync(request)); + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + } + + public static async Task RunSpecialStatusMessageAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var echoStatus = new EchoStatus + { + Code = 2, + Message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" + }; + + try + { + await client.UnaryCallAsync(new SimpleRequest + { + ResponseStatus = echoStatus + }); + Assert.Fail(); + } + catch (RpcException e) + { + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + } + + public static async Task RunUnimplementedService(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var e = await ExceptionAssert.ThrowsAsync(async () => await client.UnimplementedCallAsync(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + } + + public static async Task RunUnimplementedMethod(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var e = await ExceptionAssert.ThrowsAsync(async () => await client.UnimplementedCallAsync(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + } + + public static async Task RunClientCompressedUnary(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var probeRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = true // lie about compression + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var e = await ExceptionAssert.ThrowsAsync(async () => await client.UnaryCallAsync(probeRequest, CreateClientCompressionMetadata(false))); + Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode); + + var compressedRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response1 = await client.UnaryCallAsync(compressedRequest, CreateClientCompressionMetadata(true)); + Assert.AreEqual(314159, response1.Payload.Body.Length); + + var uncompressedRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = false + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response2 = await client.UnaryCallAsync(uncompressedRequest, CreateClientCompressionMetadata(false)); + Assert.AreEqual(314159, response2.Payload.Body.Length); + } + + public static async Task RunClientCompressedStreamingAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + try + { + var probeCall = client.StreamingInputCall(CreateClientCompressionMetadata(false)); + await probeCall.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + Payload = CreateZerosPayload(27182) + }); + + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await probeCall; + Assert.Fail(); + } + catch (RpcException e) + { + Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode); + } + + var call = client.StreamingInputCall(CreateClientCompressionMetadata(true)); + await call.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + Payload = CreateZerosPayload(27182) + }); + + call.RequestStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress); + await call.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = false + }, + Payload = CreateZerosPayload(45904) + }); + await call.RequestStream.CompleteAsync(); + + var response = await call.ResponseAsync; + Assert.AreEqual(73086, response.AggregatedPayloadSize); + } + + public static async Task RunServerCompressedUnary(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + ResponseCompressed = new BoolValue { Value = true } + }; + var response = await client.UnaryCallAsync(request); + + // Compression of response message is not verified because there is no API available + Assert.AreEqual(314159, response.Payload.Body.Length); + + request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + ResponseCompressed = new BoolValue { Value = false } + }; + response = await client.UnaryCallAsync(request); + + // Compression of response message is not verified because there is no API available + Assert.AreEqual(314159, response.Payload.Body.Length); + } + + public static async Task RunServerCompressedStreamingAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient(channel); + + var bodySizes = new List { 31415, 92653 }; + + var request = new StreamingOutputCallRequest + { + ResponseParameters = { bodySizes.Select((size) => new ResponseParameters { Size = size, Compressed = new BoolValue { Value = true } }) } + }; + + using (var call = client.StreamingOutputCall(request)) + { + // Compression of response message is not verified because there is no API available + var responseList = await call.ResponseStream.ToListAsync(); + CollectionAssert.AreEqual(bodySizes, responseList.Select((item) => item.Payload.Body.Length).ToList()); + } + } + + private static Payload CreateZerosPayload(int size) + { + return new Payload { Body = ByteString.CopyFrom(new byte[size]) }; + } + + private static Metadata CreateClientCompressionMetadata(bool compressed) + { + var algorithmName = compressed ? "gzip" : "identity"; + return new Metadata + { + { new Metadata.Entry(CompressionRequestAlgorithmMetadataKey, algorithmName) } + }; + } + + private static Metadata CreateTestMetadata() + { + return new Metadata + { + {"x-grpc-test-echo-initial", "test_initial_metadata_value"}, + {"x-grpc-test-echo-trailing-bin", new byte[] {0xab, 0xab, 0xab}} + }; + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/TestCredentials.cs b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/TestCredentials.cs new file mode 100644 index 0000000000000..28b0bffd4e81c --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/grpc-dotnet/testassets/Shared/TestCredentials.cs @@ -0,0 +1,74 @@ +#region Copyright notice and license + +// Copyright 2015 gRPC authors. +// +// Licensed 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. + +#endregion + +using System.Reflection; +using Grpc.Core; + +namespace Grpc.Shared.TestAssets +{ + /// + /// SSL Credentials for testing. + /// + public static class TestCredentials + { + public const string DefaultHostOverride = "foo.test.google.fr"; + + public static string ClientCertAuthorityPath + { + get + { + return GetPath("data/ca.pem"); + } + } + + public static string ServerCertChainPath + { + get + { + return GetPath("data/server1.pem"); + } + } + + public static string ServerPrivateKeyPath + { + get + { + return GetPath("data/server1.key"); + } + } + + public static SslCredentials CreateSslCredentials() + { + return new SslCredentials(File.ReadAllText(ClientCertAuthorityPath)); + } + + public static SslServerCredentials CreateSslServerCredentials() + { + var keyCertPair = new KeyCertificatePair( + File.ReadAllText(ServerCertChainPath), + File.ReadAllText(ServerPrivateKeyPath)); + return new SslServerCredentials(new[] { keyCertPair }); + } + + private static string GetPath(string relativePath) + { + var assemblyDir = Path.GetDirectoryName(typeof(TestCredentials).GetTypeInfo().Assembly.Location); + return Path.Combine(assemblyDir!, relativePath); + } + } +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/raw/ca.pem b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/raw/ca.pem new file mode 100644 index 0000000000000..49d39cd8ed5f8 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/raw/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw +MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01 +diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO +Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k +QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c +qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV +LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud +DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a +THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S +CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9 +/OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt +bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw +eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw== +-----END CERTIFICATE----- diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/raw/server1.pem b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/raw/server1.pem new file mode 100644 index 0000000000000..88244f856c622 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/raw/server1.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtDCCApygAwIBAgIUbJfTREJ6k6/+oInWhV1O1j3ZT0IwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw +MDMxODAzMTA0MloXDTMwMDMxNjAzMTA0MlowZTELMAkGA1UEBhMCVVMxETAPBgNV +BAgMCElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRUwEwYDVQQKDAxFeGFtcGxl +LCBDby4xGjAYBgNVBAMMESoudGVzdC5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5xOONxJJ8b8Qauvob5/7dPYZfIcd+uhAWL2ZlTPz +Qvu4oF0QI4iYgP5iGgry9zEtCM+YQS8UhiAlPlqa6ANxgiBSEyMHH/xE8lo/+caY +GeACqy640Jpl/JocFGo3xd1L8DCawjlaj6eu7T7T/tpAV2qq13b5710eNRbCAfFe +8yALiGQemx0IYhlZXNbIGWLBNhBhvVjJh7UvOqpADk4xtl8o5j0xgMIRg6WJGK6c +6ffSIg4eP1XmovNYZ9LLEJG68tF0Q/yIN43B4dt1oq4jzSdCbG4F1EiykT2TmwPV +YDi8tml6DfOCDGnit8svnMEmBv/fcPd31GSbXjF8M+KGGQIDAQABo2swaTAJBgNV +HRMEAjAAMAsGA1UdDwQEAwIF4DBPBgNVHREESDBGghAqLnRlc3QuZ29vZ2xlLmZy +ghh3YXRlcnpvb2kudGVzdC5nb29nbGUuYmWCEioudGVzdC55b3V0dWJlLmNvbYcE +wKgBAzANBgkqhkiG9w0BAQsFAAOCAQEAS8hDQA8PSgipgAml7Q3/djwQ644ghWQv +C2Kb+r30RCY1EyKNhnQnIIh/OUbBZvh0M0iYsy6xqXgfDhCB93AA6j0i5cS8fkhH +Jl4RK0tSkGQ3YNY4NzXwQP/vmUgfkw8VBAZ4Y4GKxppdATjffIW+srbAmdDruIRM +wPeikgOoRrXf0LA1fi4TqxARzeRwenQpayNfGHTvVF9aJkl8HoaMunTAdG5pIVcr +9GKi/gEMpXUJbbVv3U5frX1Wo4CFo+rZWJ/LyCMeb0jciNLxSdMwj/E/ZuExlyeZ +gc9ctPjSMvgSyXEKv6Vwobleeg88V2ZgzenziORoWj4KszG/lbQZvg== +-----END CERTIFICATE----- diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/xml/network_security_config.xml b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/xml/network_security_config.xml new file mode 100644 index 0000000000000..6ae87169b9e1e --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file