From 8afb78fb9573dd6bc62bd2bed329b4c68a00c6a8 Mon Sep 17 00:00:00 2001 From: James Newton-King <james@newtonking.com> Date: Sat, 21 Mar 2020 15:32:45 +1300 Subject: [PATCH] Add gRPC interop tests (#17040) * WIP add interop tests * Clean up * Move test project into build infrastructure * Clean up * Remove hardcoded paths * Clean up * Fix build * Add copyright notices * Update azure template * Fix build * Fix build? * Fix build? * Add gRPC interop tests to CI - Convert to using references managed by build infrastructure - Use produced AspNetCore.App shared framework - Save server logs - Dynamically bind to ports - Ensure InteropWebsite is built in the same configuration as the test project * Cleanup * Rebase fix * Include tests assets in build directory for Helix * Incorporate changes in ProcessEx * Include Grpc test in regular build * Fixup * Test * exe doesn't always exist * Capture logs on helix * Maybe this will work * There are two application started messages * Derp * Cleanup * Update directory * Add interop tests to more pipelines * mkdir Co-authored-by: John Luo <johluo@microsoft.com> --- .azure/pipelines/ci.yml | 6 +- .azure/pipelines/quarantined-tests.yml | 2 +- Directory.Build.targets | 2 + eng/Build.props | 7 + eng/Dependencies.props | 7 +- eng/Versions.props | 6 +- eng/helix/content/runtests.sh | 2 +- eng/targets/FunctionalTestAsset.targets | 9 + eng/targets/FunctionalTestWithAssets.targets | 27 + src/Grpc/Grpc.sln | 46 + src/Grpc/THIRD-PARTY-NOTICES | 26 + src/Grpc/build.cmd | 3 + src/Grpc/build.sh | 7 + src/Grpc/startvs.cmd | 3 + .../InteropTests/Helpers/ClientProcess.cs | 62 ++ .../Helpers/InteropTestsFixture.cs | 44 + .../InteropTests/Helpers/WebsiteProcess.cs | 93 ++ src/Grpc/test/InteropTests/InteropTests.cs | 80 ++ .../test/InteropTests/InteropTests.csproj | 21 + .../test/testassets/InteropClient/Assert.cs | 131 +++ .../InteropClient/AsyncStreamExtensions.cs | 86 ++ .../InteropClient/IChannelWrapper.cs | 46 + .../testassets/InteropClient/InteropClient.cs | 899 ++++++++++++++++++ .../InteropClient/InteropClient.csproj | 24 + .../test/testassets/InteropClient/Program.cs | 32 + .../testassets/InteropClient/RunTests.ps1 | 53 ++ .../InteropWebsite/AsyncStreamExtensions.cs | 41 + .../InteropWebsite/Directory.Build.targets | 24 + .../InteropWebsite/InteropWebsite.csproj | 19 + .../test/testassets/InteropWebsite/Program.cs | 63 ++ .../test/testassets/InteropWebsite/Startup.cs | 49 + .../InteropWebsite/TestServiceImpl.cs | 149 +++ .../testassets/Proto/grpc/testing/empty.proto | 28 + .../Proto/grpc/testing/messages.proto | 165 ++++ .../testassets/Proto/grpc/testing/test.proto | 79 ++ src/Grpc/test/testassets/README.md | 3 + .../test/Helpers/AspNetProcess.cs | 1 + .../test/Helpers/ErrorMessages.cs | 2 + src/ProjectTemplates/test/Helpers/Project.cs | 1 + .../test/Helpers/TemplatePackageInstaller.cs | 1 + .../SpaTemplateTest/SpaTemplateTestBase.cs | 1 + .../Helpers => Shared/Process}/ProcessEx.cs | 6 +- 42 files changed, 2346 insertions(+), 10 deletions(-) create mode 100644 eng/targets/FunctionalTestAsset.targets create mode 100644 eng/targets/FunctionalTestWithAssets.targets create mode 100644 src/Grpc/Grpc.sln create mode 100644 src/Grpc/THIRD-PARTY-NOTICES create mode 100644 src/Grpc/build.cmd create mode 100644 src/Grpc/build.sh create mode 100644 src/Grpc/startvs.cmd create mode 100644 src/Grpc/test/InteropTests/Helpers/ClientProcess.cs create mode 100644 src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs create mode 100644 src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs create mode 100644 src/Grpc/test/InteropTests/InteropTests.cs create mode 100644 src/Grpc/test/InteropTests/InteropTests.csproj create mode 100644 src/Grpc/test/testassets/InteropClient/Assert.cs create mode 100644 src/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs create mode 100644 src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs create mode 100644 src/Grpc/test/testassets/InteropClient/InteropClient.cs create mode 100644 src/Grpc/test/testassets/InteropClient/InteropClient.csproj create mode 100644 src/Grpc/test/testassets/InteropClient/Program.cs create mode 100644 src/Grpc/test/testassets/InteropClient/RunTests.ps1 create mode 100644 src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs create mode 100644 src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets create mode 100644 src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj create mode 100644 src/Grpc/test/testassets/InteropWebsite/Program.cs create mode 100644 src/Grpc/test/testassets/InteropWebsite/Startup.cs create mode 100644 src/Grpc/test/testassets/InteropWebsite/TestServiceImpl.cs create mode 100644 src/Grpc/test/testassets/Proto/grpc/testing/empty.proto create mode 100644 src/Grpc/test/testassets/Proto/grpc/testing/messages.proto create mode 100644 src/Grpc/test/testassets/Proto/grpc/testing/test.proto create mode 100644 src/Grpc/test/testassets/README.md rename src/{ProjectTemplates/test/Helpers => Shared/Process}/ProcessEx.cs (99%) diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index 9b491337a97..6c4e6adbcde 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -674,9 +674,9 @@ stages: # Build the shared framework - script: ./build.cmd -ci -all -pack -arch x64 -buildNative /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log /bl:artifacts/log/helix.build.x64.binlog displayName: Build shared fx - - script: .\restore.cmd -ci + - script: .\restore.cmd -ci /p:BuildInteropProjects=true displayName: Restore - - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl + - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildInteropProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl displayName: Run build.cmd helix target env: HelixApiAccessToken: $(HelixApiAccessToken) # Needed for internal queues @@ -701,7 +701,7 @@ stages: # Build the x86 shared framework - script: .\restore.cmd -ci displayName: Restore - - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsHelixJob=true /p:IsHelixDaily=true /p:BuildAllProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl + - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsHelixJob=true /p:IsHelixDaily=true /p:BuildAllProjects=true /p:BuildInteropProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl displayName: Run build.cmd helix target env: HelixApiAccessToken: $(HelixApiAccessToken) # Needed for internal queues diff --git a/.azure/pipelines/quarantined-tests.yml b/.azure/pipelines/quarantined-tests.yml index 415853ed8e1..791283efd6f 100644 --- a/.azure/pipelines/quarantined-tests.yml +++ b/.azure/pipelines/quarantined-tests.yml @@ -35,7 +35,7 @@ jobs: displayName: Build shared fx - script: .\restore.cmd -ci displayName: Restore - - script: .\build.cmd -ci -NoRestore -test -noBuildJava -projects eng\helix\helix.proj /p:RunQuarantinedTests=true /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl + - script: .\build.cmd -ci -NoRestore -test -noBuildJava -projects eng\helix\helix.proj /p:RunQuarantinedTests=true /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildInteropProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl displayName: Run build.cmd helix target env: HelixApiAccessToken: $(HelixApiAccessToken) # Needed for internal queues diff --git a/Directory.Build.targets b/Directory.Build.targets index a62e3ed6a48..9a30afbb8e0 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -167,5 +167,7 @@ <Import Project="eng\targets\Npm.Common.targets" Condition="'$(MSBuildProjectExtension)' == '.npmproj'" /> <Import Project="eng\targets\ReferenceAssembly.targets" Condition=" $(HasReferenceAssembly) " /> <Import Project="eng\targets\Helix.targets" Condition="'$(IsTestProject)' == 'true'" /> + <Import Project="eng\targets\FunctionalTestAsset.targets" Condition="'$(IsTestAssetProject)' == 'true'" /> + <Import Project="eng\targets\FunctionalTestWithAssets.targets" Condition="'$(ContainsFunctionalTestAssets)' == 'true'" /> </Project> diff --git a/eng/Build.props b/eng/Build.props index 4c0542db15d..6f1b3f19089 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -123,6 +123,13 @@ <ProjectToBuild Condition=" '$(BuildJava)' == 'true'" Include="@(JavaProjects)" Exclude="@(ProjectToExclude)" /> <ProjectToExclude Condition=" '$(BuildJava)' != 'true'" Include="@(JavaProjects)" /> + <!-- These interop test projects can only be restored and built after the shared framework is built --> + <InteropProjects Include="$(RepoRoot)src\Grpc\**\*.csproj" + Exclude="@(ProjectToExclude)" /> + + <ProjectToBuild Condition=" '$(BuildInteropProjects)' == 'true'" Include="@(InteropProjects)" Exclude="@(ProjectToExclude)" /> + <ProjectToExclude Condition=" '$(BuildInteropProjects)' != 'true'" Include="@(InteropProjects)" /> + <!-- Use caution to avoid deep recursion. If the globbing pattern picks up something which exceeds MAX_PATH, the entire pattern will silently fail to evaluate correctly. diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 37c0b6a4717..d02e5158ae1 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -154,8 +154,13 @@ and are generated based on the last package release. <ItemGroup Label="External dependencies" Condition="'$(DotNetBuildFromSource)' != 'true'"> <LatestPackageReference Include="AngleSharp" Version="$(AngleSharpPackageVersion)" /> <LatestPackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" /> + <LatestPackageReference Include="CommandLineParser" Version="$(CommandLineParserPackageVersion)" /> <LatestPackageReference Include="FSharp.Core" Version="$(FSharpCorePackageVersion)" /> - <LatestPackageReference Include="Google.ProtoBuf" Version="$(GoogleProtoBufPackageVersion)" /> + <LatestPackageReference Include="Google.Protobuf" Version="$(GoogleProtobufPackageVersion)" /> + <LatestPackageReference Include="Grpc.AspNetCore" Version="$(GrpcAspNetCorePackageVersion)" /> + <LatestPackageReference Include="Grpc.Auth" Version="$(GrpcAuthPackageVersion)" /> + <LatestPackageReference Include="Grpc.Net.Client" Version="$(GrpcNetClientPackageVersion)" /> + <LatestPackageReference Include="Grpc.Tools" Version="$(GrpcToolsPackageVersion)" /> <LatestPackageReference Include="IdentityServer4" Version="$(IdentityServer4PackageVersion)" /> <LatestPackageReference Include="IdentityServer4.AspNetIdentity" Version="$(IdentityServer4AspNetIdentityPackageVersion)" /> <LatestPackageReference Include="IdentityServer4.EntityFramework" Version="$(IdentityServer4EntityFrameworkPackageVersion)" /> diff --git a/eng/Versions.props b/eng/Versions.props index 63bf24ae901..a4b0b0a4840 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -217,9 +217,13 @@ <AngleSharpPackageVersion>0.9.9</AngleSharpPackageVersion> <BenchmarkDotNetPackageVersion>0.12.0</BenchmarkDotNetPackageVersion> <CastleCorePackageVersion>4.2.1</CastleCorePackageVersion> + <CommandLineParserPackageVersion>2.3.0</CommandLineParserPackageVersion> <FSharpCorePackageVersion>4.2.1</FSharpCorePackageVersion> - <GoogleProtobufPackageVersion>3.8.0</GoogleProtobufPackageVersion> + <GoogleProtobufPackageVersion>3.10.0</GoogleProtobufPackageVersion> <GrpcAspNetCorePackageVersion>2.27.0</GrpcAspNetCorePackageVersion> + <GrpcAuthPackageVersion>2.27.0</GrpcAuthPackageVersion> + <GrpcNetClientPackageVersion>2.27.0</GrpcNetClientPackageVersion> + <GrpcToolsPackageVersion>2.27.0</GrpcToolsPackageVersion> <IdentityServer4AspNetIdentityPackageVersion>3.0.0</IdentityServer4AspNetIdentityPackageVersion> <IdentityServer4EntityFrameworkPackageVersion>3.0.0</IdentityServer4EntityFrameworkPackageVersion> <IdentityServer4PackageVersion>3.0.0</IdentityServer4PackageVersion> diff --git a/eng/helix/content/runtests.sh b/eng/helix/content/runtests.sh index 9ff0133e34f..7788f800f9c 100644 --- a/eng/helix/content/runtests.sh +++ b/eng/helix/content/runtests.sh @@ -41,7 +41,7 @@ echo "Creating nugetRestore directory: $NUGET_RESTORE" mkdir $NUGET_RESTORE mkdir logs -ls -la +ls -laR RESET="\033[0m" RED="\033[0;31m" diff --git a/eng/targets/FunctionalTestAsset.targets b/eng/targets/FunctionalTestAsset.targets new file mode 100644 index 00000000000..04b35133412 --- /dev/null +++ b/eng/targets/FunctionalTestAsset.targets @@ -0,0 +1,9 @@ +<Project> + <Target Name="CollectFunctionalTestPayload" DependsOnTargets="Publish" Returns="@(DependencyPayload)" > + <ItemGroup> + <DependencyPayload Include="@(ResolvedFileToPublish)"> + <RelativePath>$(MSBuildProjectName)\%(ResolvedFileToPublish.RelativePath)</RelativePath> + </DependencyPayload> + </ItemGroup> + </Target> +</Project> \ No newline at end of file diff --git a/eng/targets/FunctionalTestWithAssets.targets b/eng/targets/FunctionalTestWithAssets.targets new file mode 100644 index 00000000000..b6194607cb4 --- /dev/null +++ b/eng/targets/FunctionalTestWithAssets.targets @@ -0,0 +1,27 @@ +<Project> + + <ItemGroup> + <ProjectReference Include="@(FunctionalTestAssetProjectReference)" ReferenceOutputAssembly="false" /> + </ItemGroup> + + <Target Name="CollectTestAssetPayload" BeforeTargets="GetCopyToOutputDirectoryItems" > + <MSBuild Targets="CollectFunctionalTestPayload" + BuildInParallel="true" + SkipNonexistentTargets="true" + Projects="%(FunctionalTestAssetProjectReference.Identity)" + RebaseOutputs="True"> + <Output TaskParameter="TargetOutputs" ItemName="DependencyPayload" /> + </MSBuild> + + <ItemGroup> + <ContentWithTargetPath + Include="@(DependencyPayload)" + Condition="'@(DependencyPayload->Count())' != '0'"> + <TargetPath>%(DependencyPayload.RelativePath)</TargetPath> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> + </ContentWithTargetPath> + </ItemGroup> + </Target> + +</Project> \ No newline at end of file diff --git a/src/Grpc/Grpc.sln b/src/Grpc/Grpc.sln new file mode 100644 index 00000000000..d1491ed3e35 --- /dev/null +++ b/src/Grpc/Grpc.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29505.145 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0FFB3605-0203-450F-80C8-F82CA2E8269F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{F5841B0A-901A-448F-9CC5-4CB393CE86AF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropTests", "test\InteropTests\InteropTests.csproj", "{90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropWebsite", "test\testassets\InteropWebsite\InteropWebsite.csproj", "{3AB7E8E4-BA36-44CE-844E-39DB66E46D45}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropClient", "test\testassets\InteropClient\InteropClient.csproj", "{66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Release|Any CPU.Build.0 = Release|Any CPU + {3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Release|Any CPU.Build.0 = Release|Any CPU + {66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {90BF37E6-B3F1-4EFC-A233-8288D8B32DD2} = {0FFB3605-0203-450F-80C8-F82CA2E8269F} + {3AB7E8E4-BA36-44CE-844E-39DB66E46D45} = {F5841B0A-901A-448F-9CC5-4CB393CE86AF} + {66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F} = {F5841B0A-901A-448F-9CC5-4CB393CE86AF} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3CAE66FD-9A59-49C2-B133-1D599225259A} + EndGlobalSection +EndGlobal diff --git a/src/Grpc/THIRD-PARTY-NOTICES b/src/Grpc/THIRD-PARTY-NOTICES new file mode 100644 index 00000000000..6b4a863467d --- /dev/null +++ b/src/Grpc/THIRD-PARTY-NOTICES @@ -0,0 +1,26 @@ +.NET Core uses third-party libraries or other resources that may be +distributed under licenses different than the .NET Core 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 interop tests +------------------------------------- + +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/Grpc/build.cmd b/src/Grpc/build.cmd new file mode 100644 index 00000000000..2406296662e --- /dev/null +++ b/src/Grpc/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. +%RepoRoot%\build.cmd -projects %~dp0**\*.*proj %* diff --git a/src/Grpc/build.sh b/src/Grpc/build.sh new file mode 100644 index 00000000000..7046bb98a0f --- /dev/null +++ b/src/Grpc/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +repo_root="$DIR/../.." +"$repo_root/build.sh" --projects "$DIR/**/*.*proj" "$@" diff --git a/src/Grpc/startvs.cmd b/src/Grpc/startvs.cmd new file mode 100644 index 00000000000..a4a22788e6f --- /dev/null +++ b/src/Grpc/startvs.cmd @@ -0,0 +1,3 @@ +@ECHO OFF + +%~dp0..\..\startvs.cmd %~dp0Grpc.sln diff --git a/src/Grpc/test/InteropTests/Helpers/ClientProcess.cs b/src/Grpc/test/InteropTests/Helpers/ClientProcess.cs new file mode 100644 index 00000000000..449342b28de --- /dev/null +++ b/src/Grpc/test/InteropTests/Helpers/ClientProcess.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Internal; +using Xunit.Abstractions; + +namespace InteropTests.Helpers +{ + public class ClientProcess : IDisposable + { + private readonly Process _process; + private readonly ProcessEx _processEx; + private readonly TaskCompletionSource<object> _startTcs; + + public ClientProcess(ITestOutputHelper output, string path, string serverPort, string testCase) + { + _process = new Process(); + _process.StartInfo = new ProcessStartInfo + { + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = "dotnet", + Arguments = @$"{path} --use_tls false --server_port {serverPort} --client_type httpclient --test_case {testCase}" + }; + _process.EnableRaisingEvents = true; + _process.OutputDataReceived += Process_OutputDataReceived; + _process.Start(); + + _processEx = new ProcessEx(output, _process); + + _startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public Task WaitForReady() + { + return _startTcs.Task; + } + + public int ExitCode => _process.ExitCode; + public Task Exited => _processEx.Exited; + + private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + var data = e.Data; + if (data != null) + { + if (data.Contains("Application started.")) + { + _startTcs.TrySetResult(null); + } + } + } + + public void Dispose() + { + _processEx.Dispose(); + } + } +} diff --git a/src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs b/src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs new file mode 100644 index 00000000000..86ffff4a079 --- /dev/null +++ b/src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace InteropTests.Helpers +{ + public class InteropTestsFixture : IDisposable + { + private WebsiteProcess _process; + + public string Path { get; set; } + + + public string ServerPort { get; private set; } + + + public async Task EnsureStarted(ITestOutputHelper output) + { + if (_process != null) + { + return; + } + + if (string.IsNullOrEmpty(Path)) + { + throw new InvalidOperationException("Path has not been set."); + } + + _process = new WebsiteProcess(Path, output); + + await _process.WaitForReady(); + + ServerPort = _process.ServerPort; + } + + public void Dispose() + { + _process.Dispose(); + } + } +} diff --git a/src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs b/src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs new file mode 100644 index 00000000000..77ac62242df --- /dev/null +++ b/src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Internal; +using Xunit.Abstractions; + +namespace InteropTests.Helpers +{ + public class WebsiteProcess : IDisposable + { + private readonly Process _process; + private readonly ProcessEx _processEx; + private readonly TaskCompletionSource<object> _startTcs; + private readonly StringBuilder _consoleOut = new StringBuilder(); + private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: .*:(?<port>\d*)$"); + + public string ServerPort { get; private set; } + + public WebsiteProcess(string path, ITestOutputHelper output) + { + _process = new Process(); + _process.StartInfo = new ProcessStartInfo + { + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = "dotnet", + Arguments = path + }; + _process.EnableRaisingEvents = true; + _process.OutputDataReceived += Process_OutputDataReceived; + _process.Start(); + + _processEx = new ProcessEx(output, _process); + + _startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public Task WaitForReady() + { + if (_processEx.HasExited) + { + return Task.FromException(new InvalidOperationException("Server is not running.")); + } + + return _startTcs.Task; + } + + private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + var data = e.Data; + if (data != null) + { + _consoleOut.AppendLine(data); + var m = NowListeningRegex.Match(data); + if (m.Success) + { + ServerPort = m.Groups["port"].Value; + } + + if (data.Contains("Application started. Press Ctrl+C to shut down.")) + { + _startTcs.TrySetResult(null); + } + } + } + + public void Dispose() + { + var attributes = Assembly.GetExecutingAssembly() + .GetCustomAttributes<AssemblyMetadataAttribute>(); + var serverLogPath = attributes.SingleOrDefault(a => a.Key == "ServerLogPath")?.Value; + if (!string.IsNullOrEmpty(serverLogPath)) + { + File.WriteAllText(serverLogPath, _consoleOut.ToString()); + } + else + { + var logDir = Path.Combine(Directory.GetCurrentDirectory(), "artifacts", "logs"); + Directory.CreateDirectory(logDir); + File.WriteAllText(Path.Combine(logDir, "InteropServer.log"), _consoleOut.ToString()); + } + _processEx.Dispose(); + } + } +} diff --git a/src/Grpc/test/InteropTests/InteropTests.cs b/src/Grpc/test/InteropTests/InteropTests.cs new file mode 100644 index 00000000000..b12ec316dc6 --- /dev/null +++ b/src/Grpc/test/InteropTests/InteropTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using InteropTests.Helpers; +using Microsoft.AspNetCore.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace InteropTests +{ + public class InteropTests : IClassFixture<InteropTestsFixture> + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30); + + private readonly string _clientPath = Path.Combine(Directory.GetCurrentDirectory(), "InteropClient", "InteropClient.dll"); + private readonly InteropTestsFixture _fixture; + private readonly ITestOutputHelper _output; + + public InteropTests(InteropTestsFixture fixture, ITestOutputHelper output) + { + var attributes = Assembly.GetExecutingAssembly() + .GetCustomAttributes<AssemblyMetadataAttribute>(); + + fixture.Path = Path.Combine(Directory.GetCurrentDirectory(), "InteropWebsite", "InteropWebsite.dll"); + + _fixture = fixture; + _output = output; + } + + [Theory] + [MemberData(nameof(TestCaseData))] + public async Task InteropTestCase(string name) + { + await _fixture.EnsureStarted(_output).TimeoutAfter(DefaultTimeout); + + using (var clientProcess = new ClientProcess(_output, _clientPath, _fixture.ServerPort, name)) + { + await clientProcess.WaitForReady().TimeoutAfter(DefaultTimeout); + + await clientProcess.Exited.TimeoutAfter(DefaultTimeout); + + Assert.Equal(0, clientProcess.ExitCode); + } + } + + #region TestData + // All interop test cases, minus GCE authentication specific tests + private static string[] AllTests = new string[] + { + "empty_unary", + "large_unary", + "client_streaming", + "server_streaming", + "ping_pong", + "empty_stream", + + "cancel_after_begin", + "cancel_after_first_response", + "timeout_on_sleeping_server", + "custom_metadata", + "status_code_and_message", + "special_status_message", + "unimplemented_service", + "unimplemented_method", + "client_compressed_unary", + "client_compressed_streaming", + "server_compressed_unary", + "server_compressed_streaming" + }; + + public static IEnumerable<object[]> TestCaseData => AllTests.Select(t => new object[] { t }); + #endregion + } +} diff --git a/src/Grpc/test/InteropTests/InteropTests.csproj b/src/Grpc/test/InteropTests/InteropTests.csproj new file mode 100644 index 00000000000..bcbd1fc4b6a --- /dev/null +++ b/src/Grpc/test/InteropTests/InteropTests.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <ContainsFunctionalTestAssets>true</ContainsFunctionalTestAssets> + <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <Compile Include="..\..\..\Shared\Process\ProcessEx.cs" Link="Helpers\ProcessEx.cs" /> + <Compile Include="..\..\..\Shared\Process\ProcessExtensions.cs" Link="Helpers\ProcessExtensions.cs" /> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(IsHelixJob)' != 'true'"> + <_Parameter1>ServerLogPath</_Parameter1> + <_Parameter2>$(ArtifactsLogDir)InteropServer.log</_Parameter2> + </AssemblyAttribute> + + <FunctionalTestAssetProjectReference Include="..\testassets\InteropClient\InteropClient.csproj" /> + <FunctionalTestAssetProjectReference Include="..\testassets\InteropWebsite\InteropWebsite.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/Grpc/test/testassets/InteropClient/Assert.cs b/src/Grpc/test/testassets/InteropClient/Assert.cs new file mode 100644 index 00000000000..8ae666f4116 --- /dev/null +++ b/src/Grpc/test/testassets/InteropClient/Assert.cs @@ -0,0 +1,131 @@ +#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; +using System.Collections; +using System.Threading.Tasks; + +namespace InteropTestsClient +{ + 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<TException> ThrowsAsync<TException>(Func<Task> 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<TException>(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/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs b/src/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs new file mode 100644 index 00000000000..00c1876fd13 --- /dev/null +++ b/src/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs @@ -0,0 +1,86 @@ +#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; +using System.Collections.Generic; +using System.Threading.Tasks; +using Grpc.Core; + +namespace InteropTestsClient +{ + /// <summary> + /// Extension methods that simplify work with gRPC streaming calls. + /// </summary> + public static class AsyncStreamExtensions + { + /// <summary> + /// Reads the entire stream and executes an async action for each element. + /// </summary> + public static async Task ForEachAsync<T>(this IAsyncStreamReader<T> streamReader, Func<T, Task> asyncAction) + where T : class + { + while (await streamReader.MoveNext().ConfigureAwait(false)) + { + await asyncAction(streamReader.Current).ConfigureAwait(false); + } + } + + /// <summary> + /// Reads the entire stream and creates a list containing all the elements read. + /// </summary> + public static async Task<List<T>> ToListAsync<T>(this IAsyncStreamReader<T> streamReader) + where T : class + { + var result = new List<T>(); + while (await streamReader.MoveNext().ConfigureAwait(false)) + { + result.Add(streamReader.Current); + } + return result; + } + + /// <summary> + /// Writes all elements from given enumerable to the stream. + /// Completes the stream afterwards unless close = false. + /// </summary> + public static async Task WriteAllAsync<T>(this IClientStreamWriter<T> streamWriter, IEnumerable<T> 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); + } + } + + /// <summary> + /// Writes all elements from given enumerable to the stream. + /// </summary> + public static async Task WriteAllAsync<T>(this IServerStreamWriter<T> streamWriter, IEnumerable<T> elements) + where T : class + { + foreach (var element in elements) + { + await streamWriter.WriteAsync(element).ConfigureAwait(false); + } + } + } +} diff --git a/src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs b/src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs new file mode 100644 index 00000000000..4cbc82532b5 --- /dev/null +++ b/src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs @@ -0,0 +1,46 @@ +#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.Net.Http; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Net.Client; + +namespace InteropTestsClient +{ + 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/Grpc/test/testassets/InteropClient/InteropClient.cs b/src/Grpc/test/testassets/InteropClient/InteropClient.cs new file mode 100644 index 00000000000..27d51f69985 --- /dev/null +++ b/src/Grpc/test/testassets/InteropClient/InteropClient.cs @@ -0,0 +1,899 @@ +#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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using CommandLine; +using Google.Apis.Auth.OAuth2; +using Google.Protobuf; +using Grpc.Auth; +using Grpc.Core; +using Grpc.Core.Utils; +using Grpc.Net.Client; +using Grpc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace InteropTestsClient +{ + public class InteropClient : IDisposable + { + internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request"; + + private class ClientOptions + { + [Option("client_type", Default = "httpclient")] + public string? ClientType { get; set; } + + [Option("server_host", Default = "localhost")] + public string? ServerHost { get; set; } + + [Option("server_host_override")] + public string? ServerHostOverride { get; set; } + + [Option("server_port" +#if DEBUG + , Default = 50052 +#endif + )] + public int ServerPort { get; set; } + + [Option("test_case" +#if DEBUG + , Default = "large_unary" +#endif + )] + public string? TestCase { get; set; } + + // Deliberately using nullable bool type to allow --use_tls=true syntax (as opposed to --use_tls) + [Option("use_tls", Default = false)] + public bool? UseTls { get; set; } + + // Deliberately using nullable bool type to allow --use_test_ca=true syntax (as opposed to --use_test_ca) + [Option("use_test_ca", Default = false)] + public bool? UseTestCa { get; set; } + + [Option("default_service_account", Required = false)] + public string? DefaultServiceAccount { get; set; } + + [Option("oauth_scope", Required = false)] + public string? OAuthScope { get; set; } + + [Option("service_account_key_file", Required = false)] + public string? ServiceAccountKeyFile { get; set; } + } + + private ServiceProvider serviceProvider; + private ILoggerFactory loggerFactory; + private ClientOptions options; + + private InteropClient(ClientOptions options) + { + this.options = options; + + var services = new ServiceCollection(); + services.AddLogging(configure => + { + configure.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + configure.AddConsole(loggerOptions => loggerOptions.IncludeScopes = true); + }); + + serviceProvider = services.BuildServiceProvider(); + + loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>(); + } + + public void Dispose() + { + serviceProvider.Dispose(); + } + + public static void Run(string[] args) + { + var parserResult = Parser.Default.ParseArguments<ClientOptions>(args) + .WithNotParsed(errors => Environment.Exit(1)) + .WithParsed(options => + { + Console.WriteLine("Use TLS: " + options.UseTls); + Console.WriteLine("Use Test CA: " + options.UseTestCa); + Console.WriteLine("Client type: " + options.ClientType); + Console.WriteLine("Server host: " + options.ServerHost); + Console.WriteLine("Server port: " + options.ServerPort); + + using (var interopClient = new InteropClient(options)) + { + interopClient.Run().GetAwaiter().GetResult(); + } + }); + } + + private async Task Run() + { + var channel = await HttpClientCreateChannel(); + await RunTestCaseAsync(channel, options); + await channel.ShutdownAsync(); + } + + private async Task<IChannelWrapper> HttpClientCreateChannel() + { + var credentials = await CreateCredentialsAsync(useTestCaOverride: false); + + string scheme; + if (!(options.UseTls ?? false)) + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + scheme = "http"; + } + else + { + scheme = "https"; + } + + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + + if (options.UseTestCa ?? false) + { + var pem = File.ReadAllText("Certs/ca.pem"); + var certData = GetBytesFromPem(pem, "CERTIFICATE"); + var cert = new X509Certificate2(certData!); + + httpClientHandler.ClientCertificates.Add(cert); + } + + var httpClient = new HttpClient(httpClientHandler); + + var channel = GrpcChannel.ForAddress($"{scheme}://{options.ServerHost}:{options.ServerPort}", new GrpcChannelOptions + { + Credentials = credentials, + HttpClient = httpClient, + LoggerFactory = loggerFactory + }); + + return new GrpcChannelWrapper(channel); + } + + private bool IsHttpClient() => string.Equals(options.ClientType, "httpclient", StringComparison.OrdinalIgnoreCase); + + private async Task<ChannelCredentials> CreateCredentialsAsync(bool? useTestCaOverride = null) + { + var credentials = ChannelCredentials.Insecure; + if (options.UseTls.GetValueOrDefault()) + { + credentials = new SslCredentials(); + } + + if (options.TestCase == "jwt_token_creds") + { + var googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsTrue(googleCredential.IsCreateScopedRequired); + credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials()); + } + + if (options.TestCase == "compute_engine_creds") + { + var googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsFalse(googleCredential.IsCreateScopedRequired); + credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials()); + } + return credentials; + } + + private TClient CreateClient<TClient>(IChannelWrapper channel) where TClient : ClientBase + { + return (TClient)Activator.CreateInstance(typeof(TClient), channel.Channel)!; + } + + private async Task RunTestCaseAsync(IChannelWrapper channel, ClientOptions options) + { + var client = CreateClient<TestService.TestServiceClient>(channel); + switch (options.TestCase) + { + case "empty_unary": + RunEmptyUnary(client); + break; + case "large_unary": + RunLargeUnary(client); + break; + case "client_streaming": + await RunClientStreamingAsync(client); + break; + case "server_streaming": + await RunServerStreamingAsync(client); + break; + case "ping_pong": + await RunPingPongAsync(client); + break; + case "empty_stream": + await RunEmptyStreamAsync(client); + break; + case "compute_engine_creds": + RunComputeEngineCreds(client, options.DefaultServiceAccount!, options.OAuthScope!); + break; + case "jwt_token_creds": + RunJwtTokenCreds(client); + break; + case "oauth2_auth_token": + await RunOAuth2AuthTokenAsync(client, options.OAuthScope!); + break; + case "per_rpc_creds": + await RunPerRpcCredsAsync(client, options.OAuthScope!); + break; + case "cancel_after_begin": + await RunCancelAfterBeginAsync(client); + break; + case "cancel_after_first_response": + await RunCancelAfterFirstResponseAsync(client); + break; + case "timeout_on_sleeping_server": + await RunTimeoutOnSleepingServerAsync(client); + break; + case "custom_metadata": + await RunCustomMetadataAsync(client); + break; + case "status_code_and_message": + await RunStatusCodeAndMessageAsync(client); + break; + case "unimplemented_service": + RunUnimplementedService(CreateClient<UnimplementedService.UnimplementedServiceClient>(channel)); + break; + case "special_status_message": + await RunSpecialStatusMessageAsync(client); + break; + case "unimplemented_method": + RunUnimplementedMethod(client); + break; + case "client_compressed_unary": + RunClientCompressedUnary(client); + break; + case "client_compressed_streaming": + await RunClientCompressedStreamingAsync(client); + break; + case "server_compressed_unary": + await RunServerCompressedUnary(client); + break; + case "server_compressed_streaming": + await RunServerCompressedStreamingAsync(client); + break; + default: + throw new ArgumentException("Unknown test case " + options.TestCase); + } + } + + public static void RunEmptyUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running empty_unary"); + var response = client.EmptyCall(new Empty()); + Assert.IsNotNull(response); + Console.WriteLine("Passed!"); + } + + public static void RunLargeUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running large_unary"); + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Console.WriteLine("Passed!"); + } + + public static async Task RunClientStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running client_streaming"); + + var bodySizes = new List<int> { 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); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunServerStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running server_streaming"); + + var bodySizes = new List<int> { 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()); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunPingPongAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running ping_pong"); + + 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()); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunEmptyStreamAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running empty_stream"); + using (var call = client.FullDuplexCall()) + { + await call.RequestStream.CompleteAsync(); + + var responseList = await call.ResponseStream.ToListAsync(); + Assert.AreEqual(0, responseList.Count); + } + Console.WriteLine("Passed!"); + } + + public static void RunComputeEngineCreds(TestService.TestServiceClient client, string defaultServiceAccount, string oauthScope) + { + Console.WriteLine("running compute_engine_creds"); + + 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 = client.UnaryCall(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); + Console.WriteLine("Passed!"); + } + + public static void RunJwtTokenCreds(TestService.TestServiceClient client) + { + Console.WriteLine("running jwt_token_creds"); + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + FillUsername = true, + }; + + // not setting credentials here because they were set on channel already + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunOAuth2AuthTokenAsync(TestService.TestServiceClient client, string oauthScope) + { + Console.WriteLine("running oauth2_auth_token"); + ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { oauthScope }); + string oauth2Token = await credential.GetAccessTokenForRequestAsync(); + + var credentials = GoogleGrpcCredentials.FromAccessToken(oauth2Token); + var request = new SimpleRequest + { + FillUsername = true, + FillOauthScope = true + }; + + var response = client.UnaryCall(request, new CallOptions(credentials: credentials)); + + Assert.IsFalse(string.IsNullOrEmpty(response.OauthScope)); + Assert.IsTrue(oauthScope.Contains(response.OauthScope)); + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunPerRpcCredsAsync(TestService.TestServiceClient client, string oauthScope) + { + Console.WriteLine("running per_rpc_creds"); + ITokenAccess googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + + var credentials = googleCredential.ToCallCredentials(); + var request = new SimpleRequest + { + FillUsername = true, + }; + + var response = client.UnaryCall(request, new CallOptions(credentials: credentials)); + + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunCancelAfterBeginAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running cancel_after_begin"); + + 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<RpcException>(() => call.ResponseAsync); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunCancelAfterFirstResponseAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running cancel_after_first_response"); + + 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); + } + } + Console.WriteLine("Passed!"); + } + + public static async Task RunTimeoutOnSleepingServerAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running timeout_on_sleeping_server"); + + 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); + } + } + Console.WriteLine("Passed!"); + } + + public static async Task RunCustomMetadataAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running custom_metadata"); + { + // 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.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes); + } + + { + // 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.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes); + } + + Console.WriteLine("Passed!"); + } + + public static async Task RunStatusCodeAndMessageAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running status_code_and_message"); + var echoStatus = new EchoStatus + { + Code = 2, + Message = "test status message" + }; + + { + // step 1: test unary call + var request = new SimpleRequest { ResponseStatus = echoStatus }; + + var e = Assert.Throws<RpcException>(() => client.UnaryCall(request)); + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + + { + // step 2: test full duplex call + var request = new StreamingOutputCallRequest { ResponseStatus = echoStatus }; + + var call = client.FullDuplexCall(); + await call.RequestStream.WriteAsync(request); + await call.RequestStream.CompleteAsync(); + + try + { + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await call.ResponseStream.ToListAsync(); + Assert.Fail(); + } + catch (RpcException e) + { + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + } + + Console.WriteLine("Passed!"); + } + + private static async Task RunSpecialStatusMessageAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running special_status_message"); + + 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); + } + + Console.WriteLine("Passed!"); + } + + public static void RunUnimplementedService(UnimplementedService.UnimplementedServiceClient client) + { + Console.WriteLine("running unimplemented_service"); + var e = Assert.Throws<RpcException>(() => client.UnimplementedCall(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + Console.WriteLine("Passed!"); + } + + public static void RunUnimplementedMethod(TestService.TestServiceClient client) + { + Console.WriteLine("running unimplemented_method"); + var e = Assert.Throws<RpcException>(() => client.UnimplementedCall(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + Console.WriteLine("Passed!"); + } + + public static void RunClientCompressedUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running client_compressed_unary"); + var probeRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = true // lie about compression + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var e = Assert.Throws<RpcException>(() => client.UnaryCall(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 = client.UnaryCall(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 = client.UnaryCall(uncompressedRequest, CreateClientCompressionMetadata(false)); + Assert.AreEqual(314159, response2.Payload.Body.Length); + + Console.WriteLine("Passed!"); + } + + public static async Task RunClientCompressedStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running client_compressed_streaming"); + 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); + + Console.WriteLine("Passed!"); + } + + public static async Task RunServerCompressedUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running server_compressed_unary"); + + 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); + + Console.WriteLine("Passed!"); + } + + public static async Task RunServerCompressedStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running server_compressed_streaming"); + + var bodySizes = new List<int> { 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()); + } + + Console.WriteLine("Passed!"); + } + + 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) } + }; + } + + // extracts the client_email field from service account file used for auth test cases + private static string GetEmailFromServiceAccountFile() + { + string keyFile = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS")!; + Assert.IsNotNull(keyFile); + var jobject = JObject.Parse(File.ReadAllText(keyFile)); + string email = jobject.GetValue("client_email").Value<string>(); + Assert.IsTrue(email.Length > 0); // spec requires nonempty client email. + return email; + } + + 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}} + }; + } + + // TODO(JamesNK): PEM loading logic from https://stackoverflow.com/a/10498045/11829 + // .NET does not have a built-in API for loading pem files + // Consider providing ca file in a different format and removing method + private byte[]? GetBytesFromPem(string pemString, string section) + { + var header = string.Format("-----BEGIN {0}-----", section); + var footer = string.Format("-----END {0}-----", section); + + var start = pemString.IndexOf(header, StringComparison.Ordinal); + if (start == -1) + { + return null; + } + + start += header.Length; + var end = pemString.IndexOf(footer, start, StringComparison.Ordinal) - start; + + if (end == -1) + { + return null; + } + + return Convert.FromBase64String(pemString.Substring(start, end)); + } + } +} diff --git a/src/Grpc/test/testassets/InteropClient/InteropClient.csproj b/src/Grpc/test/testassets/InteropClient/InteropClient.csproj new file mode 100644 index 00000000000..274295c8386 --- /dev/null +++ b/src/Grpc/test/testassets/InteropClient/InteropClient.csproj @@ -0,0 +1,24 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> + <Nullable>enable</Nullable> + <LangVersion>8.0</LangVersion> + </PropertyGroup> + + <ItemGroup> + <Protobuf Include="..\Proto\grpc\testing\test.proto" GrpcServices="Client" Link="Protos\test.proto" /> + <Protobuf Include="..\Proto\grpc\testing\empty.proto" GrpcServices="None" Link="Protos\empty.proto" /> + <Protobuf Include="..\Proto\grpc\testing\messages.proto" GrpcServices="None" Link="Protos\messages.proto" /> + + <Reference Include="CommandLineParser" /> + <Reference Include="Google.Protobuf" /> + <Reference Include="Grpc.Auth" /> + <Reference Include="Grpc.Net.Client" /> + <Reference Include="Grpc.Tools" PrivateAssets="All" /> + <Reference Include="Microsoft.Extensions.DependencyInjection" /> + <Reference Include="Microsoft.Extensions.Logging.Console" /> + </ItemGroup> + +</Project> diff --git a/src/Grpc/test/testassets/InteropClient/Program.cs b/src/Grpc/test/testassets/InteropClient/Program.cs new file mode 100644 index 00000000000..3fc3d9d9757 --- /dev/null +++ b/src/Grpc/test/testassets/InteropClient/Program.cs @@ -0,0 +1,32 @@ +#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; + +namespace InteropTestsClient +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Application started."); + + InteropClient.Run(args); + } + } +} diff --git a/src/Grpc/test/testassets/InteropClient/RunTests.ps1 b/src/Grpc/test/testassets/InteropClient/RunTests.ps1 new file mode 100644 index 00000000000..faa46d98c1b --- /dev/null +++ b/src/Grpc/test/testassets/InteropClient/RunTests.ps1 @@ -0,0 +1,53 @@ +Param +( + [bool]$use_tls = $false +) + +$allTests = + "empty_unary", + "large_unary", + "client_streaming", + "server_streaming", + "ping_pong", + "empty_stream", + + #"compute_engine_creds", + #"jwt_token_creds", + #"oauth2_auth_token", + #"per_rpc_creds", + + "cancel_after_begin", + "cancel_after_first_response", + "timeout_on_sleeping_server", + "custom_metadata", + "status_code_and_message", + "special_status_message", + "unimplemented_service", + "unimplemented_method", + "client_compressed_unary", + "client_compressed_streaming", + "server_compressed_unary", + "server_compressed_streaming" + +Write-Host "Running $($allTests.Count) tests" -ForegroundColor Cyan +Write-Host "Use TLS: $use_tls" -ForegroundColor Cyan +Write-Host + +foreach ($test in $allTests) +{ + Write-Host "Running $test" -ForegroundColor Cyan + + if (!$use_tls) + { + dotnet run --use_tls false --server_port 50052 --client_type httpclient --test_case $test + } + else + { + # Certificate is for test.google.com host. To run locally, setup the host file to point test.google.com to 127.0.0.1 + dotnet run --use_tls true --server_port 50052 --client_type httpclient --test_case $test --server_host test.google.com + } + + Write-Host +} + +Write-Host "Done" -ForegroundColor Cyan \ No newline at end of file diff --git a/src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs b/src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs new file mode 100644 index 00000000000..1f296cb7ba3 --- /dev/null +++ b/src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs @@ -0,0 +1,41 @@ +#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; +using System.Collections.Generic; +using System.Threading.Tasks; +using Grpc.Core; + +namespace InteropTestsWebsite +{ + // Implementation copied from https://github.com/grpc/grpc/blob/master/src/csharp/Grpc.Core/Utils/AsyncStreamExtensions.cs + internal static class AsyncStreamExtensions + { + /// <summary> + /// Reads the entire stream and executes an async action for each element. + /// </summary> + public static async Task ForEachAsync<T>(this IAsyncStreamReader<T> streamReader, Func<T, Task> asyncAction) + where T : class + { + while (await streamReader.MoveNext().ConfigureAwait(false)) + { + await asyncAction(streamReader.Current).ConfigureAwait(false); + } + } + } +} diff --git a/src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets b/src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets new file mode 100644 index 00000000000..2f889b40eae --- /dev/null +++ b/src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets @@ -0,0 +1,24 @@ +<Project> + <!-- Skip the parent folder to prevent getting test package references. --> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\..\, Directory.Build.targets))\Directory.Build.targets" /> + + <PropertyGroup> + <RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);$(ArtifactsShippingPackagesDir)</RestoreAdditionalProjectSources> + <MicrosoftAspNetCoreAppRefPackageVersion Condition="'$(IsTargetingPackBuilding)' != 'false'">$(TargetingPackVersion)</MicrosoftAspNetCoreAppRefPackageVersion> + <MicrosoftAspNetCoreAppRefPackageVersion Condition="'$(IsTargetingPackBuilding)' == 'false'">$(AspNetCoreBaselineVersion)</MicrosoftAspNetCoreAppRefPackageVersion> + </PropertyGroup> + + <!-- Use the shared framework that was produced --> + <ItemGroup> + <KnownFrameworkReference + Include="Microsoft.AspNetCore.App" + TargetFramework="$(DefaultNetCoreTargetFramework)" + RuntimeFrameworkName="Microsoft.AspNetCore.App" + TargetingPackName="Microsoft.AspNetCore.App.Ref" + RuntimePackNamePatterns="Microsoft.AspNetCore.App.Runtime.**RID**" + DefaultRuntimeFrameworkVersion="$(SharedFxVersion)" + LatestRuntimeFrameworkVersion="$(SharedFxVersion)" + TargetingPackVersion="$(MicrosoftAspNetCoreAppRefPackageVersion)" + RuntimePackRuntimeIdentifiers="$(SupportedRuntimeIdentifiers)" /> + </ItemGroup> +</Project> diff --git a/src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj b/src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj new file mode 100644 index 00000000000..4b8d6d38509 --- /dev/null +++ b/src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> + <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> + <GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute> + <Nullable>enable</Nullable> + <LangVersion>8.0</LangVersion> + </PropertyGroup> + + <ItemGroup> + <Protobuf Include="..\Proto\grpc\testing\test.proto" GrpcServices="Server" Link="Protos\test.proto" /> + <Protobuf Include="..\Proto\grpc\testing\empty.proto" GrpcServices="None" Link="Protos\empty.proto" /> + <Protobuf Include="..\Proto\grpc\testing\messages.proto" GrpcServices="None" Link="Protos\messages.proto" /> + + <Reference Include="Grpc.AspNetCore" /> + </ItemGroup> + +</Project> diff --git a/src/Grpc/test/testassets/InteropWebsite/Program.cs b/src/Grpc/test/testassets/InteropWebsite/Program.cs new file mode 100644 index 00000000000..fffaf75c559 --- /dev/null +++ b/src/Grpc/test/testassets/InteropWebsite/Program.cs @@ -0,0 +1,63 @@ +#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; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace InteropTestsWebsite +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel((context, options) => + { + // Support --port and --use_tls cmdline arguments normally supported + // by gRPC interop servers. + var useTls = context.Configuration.GetValue("use_tls", false); + + options.Limits.MinRequestBodyDataRate = null; + options.ListenAnyIP(0, listenOptions => + { + 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, "1111"); + } + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + webBuilder.UseStartup<Startup>(); + }); + } +} diff --git a/src/Grpc/test/testassets/InteropWebsite/Startup.cs b/src/Grpc/test/testassets/InteropWebsite/Startup.cs new file mode 100644 index 00000000000..4715d1fbbca --- /dev/null +++ b/src/Grpc/test/testassets/InteropWebsite/Startup.cs @@ -0,0 +1,49 @@ +#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; +using Grpc.Testing; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +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(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostApplicationLifetime applicationLifetime) + { + // Required to notify test infrastructure that it can begin tests + applicationLifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started.")); + + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService<TestServiceImpl>(); + }); + } + } +} diff --git a/src/Grpc/test/testassets/InteropWebsite/TestServiceImpl.cs b/src/Grpc/test/testassets/InteropWebsite/TestServiceImpl.cs new file mode 100644 index 00000000000..918500cd256 --- /dev/null +++ b/src/Grpc/test/testassets/InteropWebsite/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 System; +using System.Linq; +using System.Threading.Tasks; +using Google.Protobuf; +using Grpc.Core; +using InteropTestsWebsite; + +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<Empty> EmptyCall(Empty request, ServerCallContext context) + { + return Task.FromResult(new Empty()); + } + + public override async Task<SimpleResponse> 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<StreamingOutputCallResponse> 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<StreamingInputCallResponse> StreamingInputCall(IAsyncStreamReader<StreamingInputCallRequest> 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<StreamingOutputCallRequest> requestStream, IServerStreamWriter<StreamingOutputCallResponse> 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<StreamingOutputCallRequest> requestStream, IServerStreamWriter<StreamingOutputCallResponse> 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 => h.Key == "grpc-encoding").Value.SingleOrDefault(); + if (expectCompressed.Value) + { + if (encoding == null || encoding == "identity") + { + throw new RpcException(new Status(StatusCode.InvalidArgument, string.Empty)); + } + } + } + } + } +} diff --git a/src/Grpc/test/testassets/Proto/grpc/testing/empty.proto b/src/Grpc/test/testassets/Proto/grpc/testing/empty.proto new file mode 100644 index 00000000000..6a0aa88dfde --- /dev/null +++ b/src/Grpc/test/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/Grpc/test/testassets/Proto/grpc/testing/messages.proto b/src/Grpc/test/testassets/Proto/grpc/testing/messages.proto new file mode 100644 index 00000000000..7b1b7286dce --- /dev/null +++ b/src/Grpc/test/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/Grpc/test/testassets/Proto/grpc/testing/test.proto b/src/Grpc/test/testassets/Proto/grpc/testing/test.proto new file mode 100644 index 00000000000..86d6ab60506 --- /dev/null +++ b/src/Grpc/test/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/Grpc/test/testassets/README.md b/src/Grpc/test/testassets/README.md new file mode 100644 index 00000000000..d7798c92695 --- /dev/null +++ b/src/Grpc/test/testassets/README.md @@ -0,0 +1,3 @@ +InteropTestsClient and InteropTestsWebsite are copied from [grpc-dotnet](https://github.com/grpc/grpc-dotnet/tree/master/testassets). + +For more information about the interop tests, see the [interop tests specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). diff --git a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs index 753eb1258a5..86724b970fb 100644 --- a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs +++ b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using AngleSharp.Dom.Html; using AngleSharp.Parser.Html; using Microsoft.AspNetCore.Certificates.Generation; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging.Abstractions; diff --git a/src/ProjectTemplates/test/Helpers/ErrorMessages.cs b/src/ProjectTemplates/test/Helpers/ErrorMessages.cs index c63f008f831..744ada299bf 100644 --- a/src/ProjectTemplates/test/Helpers/ErrorMessages.cs +++ b/src/ProjectTemplates/test/Helpers/ErrorMessages.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Internal; + namespace Templates.Test.Helpers { internal static class ErrorMessages diff --git a/src/ProjectTemplates/test/Helpers/Project.cs b/src/ProjectTemplates/test/Helpers/Project.cs index 3fdcdf9d0ce..b16801a5f74 100644 --- a/src/ProjectTemplates/test/Helpers/Project.cs +++ b/src/ProjectTemplates/test/Helpers/Project.cs @@ -10,6 +10,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.CommandLineUtils; using Xunit; using Xunit.Abstractions; diff --git a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs index 3eed004ef60..5c94fde2b49 100644 --- a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs +++ b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.CommandLineUtils; using Xunit; using Xunit.Abstractions; diff --git a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs index bd393b8d586..d702e01be1e 100644 --- a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs +++ b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs @@ -10,6 +10,7 @@ using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.E2ETesting; +using Microsoft.AspNetCore.Internal; using Newtonsoft.Json.Linq; using OpenQA.Selenium; using Templates.Test.Helpers; diff --git a/src/ProjectTemplates/test/Helpers/ProcessEx.cs b/src/Shared/Process/ProcessEx.cs similarity index 99% rename from src/ProjectTemplates/test/Helpers/ProcessEx.cs rename to src/Shared/Process/ProcessEx.cs index db132bdafe0..c1743a2f0a9 100644 --- a/src/ProjectTemplates/test/Helpers/ProcessEx.cs +++ b/src/Shared/Process/ProcessEx.cs @@ -15,7 +15,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Internal; using Xunit.Abstractions; -namespace Templates.Test.Helpers +namespace Microsoft.AspNetCore.Internal { internal class ProcessEx : IDisposable { @@ -100,7 +100,7 @@ namespace Templates.Test.Helpers } startInfo.EnvironmentVariables["NUGET_PACKAGES"] = NUGET_PACKAGES; - + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("helix"))) { startInfo.EnvironmentVariables["NUGET_FALLBACK_PACKAGES"] = Environment.GetEnvironmentVariable("NUGET_FALLBACK_PACKAGES"); @@ -195,7 +195,7 @@ namespace Templates.Test.Helpers } } - private static string GetNugetPackagesRestorePath() => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NUGET_RESTORE"))) + private static string GetNugetPackagesRestorePath() => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NUGET_RESTORE"))) ? typeof(ProcessEx).Assembly .GetCustomAttributes<AssemblyMetadataAttribute>() .First(attribute => attribute.Key == "TestPackageRestorePath") -- GitLab