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