Initial version of Grpc.Tools & Msbuild integration tests (#31638)

* Inital Grpc.Tools unit tests

* refining the tests

* Get tests to run on Linux

* further grpc.tools tests changes

* changes for relative/absoulte paths

* Simplify running on just one framework

* Add Protobuf.MSBuild.dll for tests to use

* fix typo in comment

* fix copyright note

* Revert "Add Protobuf.MSBuild.dll for tests to use"

This reverts commit 542756cade.

* cleanup Directory.Build.props

* fix location of tasksAssembly

* cleanup in preparing paths

* add test-out to .gitignore

* many simplifications to MsBuildIntegrationTest

* update expected test results data

* many improvements to fakeprotoc.py

* yapf code

* fix copyright licenses

* reintroduce normalized paths

* minor fixes

* attempt simplify Directory.Build.targets

* newline at EOF

* simplify result comparison

* add a TODO

* further simplify MsBuildIntegrationTest test harness

* avoid internal symbol conflict by diambiguating test protos

* add TODO

* style fixes

* one more fixup

Co-authored-by: Jan Tattermusch <jtattermusch@google.com>
pull/32047/head
tony 2 years ago committed by GitHub
parent 06faf44d0b
commit 0ff115e49c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/csharp/.gitignore
  2. 9
      src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj
  3. 27
      src/csharp/Grpc.Tools.Tests/IntegrationTests/Directory.Build.props
  4. 11
      src/csharp/Grpc.Tools.Tests/IntegrationTests/Directory.Build.targets
  5. 35
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestAtInPath/@protos/file.proto
  6. 30
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestAtInPath/Program.cs
  7. 31
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestAtInPath/expected.json
  8. 13
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestAtInPath/msbuildtest.csproj
  9. 30
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestMultipleProtos/Program.cs
  10. 79
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestMultipleProtos/expected.json
  11. 35
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestMultipleProtos/file.proto
  12. 13
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestMultipleProtos/msbuildtest.csproj
  13. 35
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestMultipleProtos/protos/another.proto
  14. 35
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestMultipleProtos/second.proto
  15. 31
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestProtoOutsideProject/api/greet.proto
  16. 30
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestProtoOutsideProject/project/Program.cs
  17. 34
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestProtoOutsideProject/project/expected.json
  18. 31
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestProtoOutsideProject/project/msbuildtest.csproj
  19. 30
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestSingleProto/Program.cs
  20. 31
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestSingleProto/expected.json
  21. 35
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestSingleProto/file.proto
  22. 13
      src/csharp/Grpc.Tools.Tests/IntegrationTests/TestSingleProto/msbuildtest.csproj
  23. 438
      src/csharp/Grpc.Tools.Tests/MsBuildIntegrationTest.cs
  24. 18
      src/csharp/Grpc.Tools.Tests/scripts/fakeprotoc.bat
  25. 353
      src/csharp/Grpc.Tools.Tests/scripts/fakeprotoc.py
  26. 5
      src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets
  27. 3
      src/csharp/tests.json

@ -15,3 +15,4 @@ test-results/
TestResult.xml
coverage_results.xml
/TestResults
/test-out/

@ -7,12 +7,21 @@
<Import Project="..\Grpc.Tools\SourceLink.csproj.include" />
<ItemGroup>
<!-- Prevent tests data files from being compiled into this project -->
<!-- TODO(jtattermusch): find a better solution for this. -->
<Compile Remove="IntegrationTests\**" />
<EmbeddedResource Remove="IntegrationTests\**" />
<None Remove="IntegrationTests\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Grpc.Tools\Grpc.Tools.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.8.3" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="NUnit; NUnitLite" Version="3.10.1" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
</ItemGroup>

@ -0,0 +1,27 @@
<!--
This overrides the Directory.Build.props in src/csharp/
since we want different settings for the integration tests
-->
<Project>
<PropertyGroup>
<LangVersion>8.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<!-- These would normally be imported from the Grpc.Tools NuGet package -->
<Import Project="$(GrpcToolsBuildDir)\_grpc\_Grpc.Tools.props"/>
<Import Project="$(GrpcToolsBuildDir)\_protobuf\Google.Protobuf.Tools.props"/>
<!-- Common properties for tests. -->
<!-- The <Protobuf_...> properties would normally be set when the files are imported
via the NuGet package.
-->
<PropertyGroup>
<Protobuf_Generator>CSharp</Protobuf_Generator>
<Protobuf_IntermediatePath>$(BaseIntermediateOutputPath)Debug/netstandard2.0</Protobuf_IntermediatePath>
</PropertyGroup>
</Project>

@ -0,0 +1,11 @@
<!--
This overrides the Directory.Build.targets in src/csharp/
since we want different settings for the integration tests, particularly
we don't want to sign assemblies
-->
<Project>
<!-- These would normally be imported from the Grpc.Tools NuGet package -->
<Import Project="$(GrpcToolsBuildDir)\_grpc\_Grpc.Tools.targets"/>
<Import Project="$(GrpcToolsBuildDir)\_protobuf\Google.Protobuf.Tools.targets"/>
</Project>

@ -0,0 +1,35 @@
// Copyright 2022 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.
syntax = "proto3";
package grpc_tools_tests.integration_tests.test_at_in_path;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloSlowly (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
int32 delay = 2;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

@ -0,0 +1,30 @@
#region Copyright notice and license
// Copyright 2022 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 TestApp
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("test app");
}
}
}

@ -0,0 +1,31 @@
{
"Files": {
"@protos/file.proto": {
"--csharp_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0/@protos"
],
"--plugin": [
"protoc-gen-grpc=dummy-plugin-not-used"
],
"--grpc_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0/@protos"
],
"--proto_path": [
"../../../Grpc.Tools/build/native/include",
"."
],
"--dependency_out": [
"REGEX:${TEST_OUT_DIR}/obj/Debug/netstandard2.0/.*_file.protodep"
],
"--error_format": [
"msvs"
],
"protofile": [
"@protos/file.proto"
]
}
},
"Metadata": {
"timestamp": "IGNORE:"
}
}

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
Note: common properties and targets for tests are imported from the
Directory.Build.props and Directory.Build.targets files in the parent directoty
-->
<!-- The protobuf compiler settings to test -->
<ItemGroup>
<Protobuf Include="**/*.proto" />
</ItemGroup>
</Project>

@ -0,0 +1,30 @@
#region Copyright notice and license
// Copyright 2022 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 TestApp
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("test app");
}
}
}

@ -0,0 +1,79 @@
{
"Files": {
"file.proto": {
"--csharp_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0"
],
"--plugin": [
"protoc-gen-grpc=dummy-plugin-not-used"
],
"--grpc_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0"
],
"--proto_path": [
"../../../Grpc.Tools/build/native/include",
"."
],
"--dependency_out": [
"REGEX:${TEST_OUT_DIR}/obj/Debug/netstandard2.0/.*_file.protodep"
],
"--error_format": [
"msvs"
],
"protofile": [
"file.proto"
]
},
"protos/another.proto": {
"--csharp_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0/protos"
],
"--plugin": [
"protoc-gen-grpc=dummy-plugin-not-used"
],
"--grpc_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0/protos"
],
"--proto_path": [
"../../../Grpc.Tools/build/native/include",
"."
],
"--dependency_out": [
"REGEX:${TEST_OUT_DIR}/obj/Debug/netstandard2.0/.*_another.protodep"
],
"--error_format": [
"msvs"
],
"protofile": [
"protos/another.proto"
]
},
"second.proto": {
"--csharp_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0"
],
"--plugin": [
"protoc-gen-grpc=dummy-plugin-not-used"
],
"--grpc_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0"
],
"--proto_path": [
"../../../Grpc.Tools/build/native/include",
"."
],
"--dependency_out": [
"REGEX:${TEST_OUT_DIR}/obj/Debug/netstandard2.0/.*_second.protodep"
],
"--error_format": [
"msvs"
],
"protofile": [
"second.proto"
]
}
},
"Metadata": {
"timestamp": "IGNORE:"
}
}

@ -0,0 +1,35 @@
// Copyright 2022 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.
syntax = "proto3";
package grpc_tools_tests.integration_tests.test_multiple_protos;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloSlowly (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
int32 delay = 2;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
Note: common properties and targets for tests are imported from the
Directory.Build.props and Directory.Build.targets files in the parent directoty
-->
<!-- The protobuf compiler settings to test -->
<ItemGroup>
<Protobuf Include="**/*.proto" />
</ItemGroup>
</Project>

@ -0,0 +1,35 @@
// Copyright 2022 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.
syntax = "proto3";
package grpc_tools_tests.integration_tests.test_multiple_protos.another;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloSlowly (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
int32 delay = 2;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

@ -0,0 +1,35 @@
// Copyright 2022 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.
syntax = "proto3";
package grpc_tools_tests.integration_tests.test_multiple_protos.second;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloSlowly (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
int32 delay = 2;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

@ -0,0 +1,31 @@
// Copyright 2022 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.
syntax = "proto3";
option csharp_namespace = "GrpcGreeter";
package grpc_tools_tests.integration_tests.test_proto_outside_project;
service Greeter {
rpc SayHello (Greet.HelloRequest) returns (Greet.HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}

@ -0,0 +1,30 @@
#region Copyright notice and license
// Copyright 2022 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 TestApp
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("test app");
}
}
}

@ -0,0 +1,34 @@
{
"Files": {
"../api/greet.proto": {
"--csharp_out": [
"${TEST_OUT_DIR}/generated"
],
"--plugin": [
"protoc-gen-grpc=dummy-plugin-not-used"
],
"--grpc_out": [
"${TEST_OUT_DIR}/generated"
],
"--grpc_opt": [
"no_server"
],
"--proto_path": [
"../../../../Grpc.Tools/build/native/include",
"../api"
],
"--dependency_out": [
"REGEX:${TEST_OUT_DIR}/obj/Debug/netstandard2.0/.*_greet.protodep"
],
"--error_format": [
"msvs"
],
"protofile": [
"../api/greet.proto"
]
}
},
"Metadata": {
"timestamp": "IGNORE:"
}
}

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
Note: common properties and targets for tests are imported from the
Directory.Build.props and Directory.Build.targets files in the parent directoty
-->
<!-- The protobuf compiler settings to test -->
<!-- From issue 29161 -->
<PropertyGroup>
<!-- full path to parent directory -->
<ParentDir>$(MSBuildThisFileDirectory)..\</ParentDir>
<ProtoApiRepoBaseDir>$(ParentDir)api\</ProtoApiRepoBaseDir>
<AutoGenOutputBaseDir>$(TestOutDir)\generated\</AutoGenOutputBaseDir>
</PropertyGroup>
<ItemGroup>
<Protobuf Include = "$(ProtoApiRepoBaseDir)\**\*.proto"
Link = "ProtoApi\%(RecursiveDir)%(Filename)%(Extension)"
Access = "Public"
ProtoCompile = "True"
ProtoRoot = "$(ProtoApiRepoBaseDir)"
CompileOutputs = "true"
OutputDir = "$(AutoGenOutputBaseDir)"
GrpcOutputDir = "$(AutoGenOutputBaseDir)"
GrpcServices = "client" />
</ItemGroup>
</Project>

@ -0,0 +1,30 @@
#region Copyright notice and license
// Copyright 2022 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 TestApp
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("test app");
}
}
}

@ -0,0 +1,31 @@
{
"Files": {
"file.proto": {
"--csharp_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0"
],
"--plugin": [
"protoc-gen-grpc=dummy-plugin-not-used"
],
"--grpc_out": [
"${TEST_OUT_DIR}/obj/Debug/netstandard2.0"
],
"--proto_path": [
"../../../Grpc.Tools/build/native/include",
"."
],
"--dependency_out": [
"REGEX:${TEST_OUT_DIR}/obj/Debug/netstandard2.0/.*_file.protodep"
],
"--error_format": [
"msvs"
],
"protofile": [
"file.proto"
]
}
},
"Metadata": {
"timestamp": "IGNORE:"
}
}

@ -0,0 +1,35 @@
// Copyright 2022 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.
syntax = "proto3";
package grpc_tools_tests.integration_tests.test_single_proto;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloSlowly (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
int32 delay = 2;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
Note: common properties and targets for tests are imported from the
Directory.Build.props and Directory.Build.targets files in the parent directoty
-->
<!-- The protobuf compiler settings to test -->
<ItemGroup>
<Protobuf Include="**/*.proto" />
</ItemGroup>
</Project>

@ -0,0 +1,438 @@
#region Copyright notice and license
// Copyright 2022 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 NUnit.Framework;
using System.Diagnostics;
using System.Reflection;
using System.Collections.Specialized;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
namespace Grpc.Tools.Tests
{
/// <summary>
/// Tests for Grpc.Tools MSBuild .target and .props files.
/// </summary>
/// <remarks>
/// The Grpc.Tools NuGet package is not tested directly, but instead the
/// same .target and .props files are included in a MSBuild project and
/// that project is built using "dotnet build" with the SDK installed on
/// the test machine.
/// <para>
/// The real protoc compiler is not called. Instead a fake protoc script is
/// called that does the minimum work needed for the build to succeed
/// (generating cs files and writing dependencies file) and also writes out
/// the arguments it was called with in a JSON file. The output is checked
/// with expected results.
/// </para>
/// </remarks>
[TestFixture]
public class MsBuildIntegrationTest
{
private const string TASKS_ASSEMBLY_PROPERTY = "_Protobuf_MsBuildAssembly";
private const string TASKS_ASSEMBLY_DLL = "Protobuf.MSBuild.dll";
private const string PROTBUF_FULLPATH_PROPERTY = "Protobuf_ProtocFullPath";
private const string PLUGIN_FULLPATH_PROPERTY = "gRPC_PluginFullPath";
private const string TOOLS_BUILD_DIR_PROPERTY = "GrpcToolsBuildDir";
private const string MSBUILD_LOG_VERBOSITY = "diagnostic"; // "diagnostic" or "detailed"
private string testId;
private string fakeProtoc;
private string grpcToolsBuildDir;
private string tasksAssembly;
private string testDataDir;
private string testProjectDir;
private string testOutBaseDir;
private string testOutDir;
[SetUp]
public void InitTest()
{
#if NET45
// We need to run these tests for one framework.
// This test class is just a driver for calling the
// "dotnet build" processes, so it doesn't matter what
// the runtime of this class actually is.
Assert.Ignore("Skipping test when NET45");
#endif
}
[Test]
public void TestSingleProto()
{
SetUpForTest(nameof(TestSingleProto));
var expectedFiles = new ExpectedFilesBuilder();
expectedFiles.Add("file.proto", "File.cs", "FileGrpc.cs");
TryRunMsBuild("TestSingleProto", expectedFiles.ToString());
}
[Test]
public void TestMultipleProtos()
{
SetUpForTest(nameof(TestMultipleProtos));
var expectedFiles = new ExpectedFilesBuilder();
// TODO(jtattermusch): add test that "duplicate" .proto file
// name (under different directories) is allowed. See https://github.com/grpc/grpc/issues/17672
expectedFiles.Add("file.proto", "File.cs", "FileGrpc.cs")
.Add("protos/another.proto", "Another.cs", "AnotherGrpc.cs")
.Add("second.proto", "Second.cs", "SecondGrpc.cs");
TryRunMsBuild("TestMultipleProtos", expectedFiles.ToString());
}
[Test]
public void TestAtInPath()
{
SetUpForTest(nameof(TestAtInPath));
var expectedFiles = new ExpectedFilesBuilder();
expectedFiles.Add("@protos/file.proto", "File.cs", "FileGrpc.cs");
TryRunMsBuild("TestAtInPath", expectedFiles.ToString());
}
[Test]
public void TestProtoOutsideProject()
{
SetUpForTest(nameof(TestProtoOutsideProject), "TestProtoOutsideProject/project");
var expectedFiles = new ExpectedFilesBuilder();
expectedFiles.Add("../api/greet.proto", "Greet.cs", "GreetGrpc.cs");
TryRunMsBuild("TestProtoOutsideProject/project", expectedFiles.ToString());
}
/// <summary>
/// Set up common paths for all the tests
/// </summary>
private void SetUpCommonPaths()
{
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
testDataDir = Path.GetFullPath($"{assemblyDir}/../../../IntegrationTests");
// Path for fake proto.
// On Windows we have to wrap the python script in a BAT script since we can only
// pass one executable name without parameters to the MSBuild
// - e.g. we can't give "python fakeprotoc.py"
var fakeProtocScript = Platform.IsWindows ? "fakeprotoc.bat" : "fakeprotoc.py";
fakeProtoc = Path.GetFullPath($"{assemblyDir}/../../../scripts/{fakeProtocScript}");
// Path for "build" directory under Grpc.Tools
grpcToolsBuildDir = Path.GetFullPath($"{assemblyDir}/../../../../Grpc.Tools/build");
// Task assembly is needed to run the extension tasks
// We use the assembly that was copied next to Grpc.Tools.Tests.dll
// as a Grpc.Tools.Tests dependency since we know it's the correct one
// and we don't have to figure out its original path (which is different
// for debug/release builds etc).
tasksAssembly = Path.Combine(assemblyDir, TASKS_ASSEMBLY_DLL);
// put test ouptput directory outside of Grpc.Tools.Tests to avoid problems with
// repeated builds.
testOutBaseDir = NormalizePath(Path.GetFullPath($"{assemblyDir}/../../../../test-out/grpc_tools_integration_tests"));
}
/// <summary>
/// Normalize path string to use just forward slashes. That makes it easier to compare paths
/// for equality in the tests.
/// </summary>
private string NormalizePath(string path)
{
return path.Replace('\\','/');
}
/// <summary>
/// Set up test specific paths
/// </summary>
/// <param name="testName">Name of the test</param>
/// <param name="testPath">Optional path to the test project</param>
private void SetUpForTest(string testName, string testPath = null)
{
if (testPath == null) {
testPath = testName;
}
SetUpCommonPaths();
testId = $"{testName}_run-{Guid.NewGuid().ToString()}";
Console.WriteLine($"TestID for test: {testId}");
// Paths for test data
testProjectDir = NormalizePath(Path.Combine(testDataDir, testPath));
testOutDir = NormalizePath(Path.Combine(testOutBaseDir, testId));
}
/// <summary>
/// Run "dotnet build" on the test's project file.
/// </summary>
/// <param name="testName">Name of test and name of directory containing the test</param>
/// <param name="filesToGenerate">Tell the fake protoc script which files to generate</param>
/// <param name="testId">A unique ID for the test run - used to create results file</param>
private void TryRunMsBuild(string testName, string filesToGenerate)
{
Directory.CreateDirectory(testOutDir);
// create the arguments for the "dotnet build"
var args = $"build -p:{TASKS_ASSEMBLY_PROPERTY}={tasksAssembly}"
+ $" -p:TestOutDir={testOutDir}"
+ $" -p:BaseOutputPath={testOutDir}/bin/"
+ $" -p:BaseIntermediateOutputPath={testOutDir}/obj/"
+ $" -p:{TOOLS_BUILD_DIR_PROPERTY}={grpcToolsBuildDir}"
+ $" -p:{PROTBUF_FULLPATH_PROPERTY}={fakeProtoc}"
+ $" -p:{PLUGIN_FULLPATH_PROPERTY}=dummy-plugin-not-used"
+ $" -fl -flp:LogFile={testOutDir}/log/msbuild.log;verbosity={MSBUILD_LOG_VERBOSITY}"
+ $" msbuildtest.csproj";
// To pass additional parameters to fake protoc process
// we need to use environment variables
var envVariables = new StringDictionary {
{ "FAKEPROTOC_PROJECTDIR", testProjectDir },
{ "FAKEPROTOC_OUTDIR", testOutDir },
{ "FAKEPROTOC_GENERATE_EXPECTED", filesToGenerate },
{ "FAKEPROTOC_TESTID", testId }
};
// Run the "dotnet build"
ProcessMsbuild(args, testProjectDir, envVariables);
// Check the results JSON matches the expected JSON
Results actualResults = Results.Read(testOutDir + "/log/results.json");
Results expectedResults = Results.Read(testProjectDir + "/expected.json");
CompareResults(expectedResults, actualResults);
}
/// <summary>
/// Run the "dotnet build" command
/// </summary>
/// <param name="args">arguments to the dotnet command</param>
/// <param name="workingDirectory">working directory</param>
/// <param name="envVariables">environment variables to set</param>
private void ProcessMsbuild(string args, string workingDirectory, StringDictionary envVariables)
{
using (var process = new Process())
{
process.StartInfo.FileName = "dotnet";
process.StartInfo.Arguments = args;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WorkingDirectory = workingDirectory;
process.StartInfo.UseShellExecute = false;
StringDictionary procEnv = process.StartInfo.EnvironmentVariables;
foreach (DictionaryEntry entry in envVariables)
{
if (!procEnv.ContainsKey((string)entry.Key))
{
procEnv.Add((string)entry.Key, (string)entry.Value);
}
}
process.OutputDataReceived += (sender, e) => {
if (e.Data != null)
{
Console.WriteLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) => {
if (e.Data != null)
{
Console.WriteLine(e.Data);
}
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.WaitForExit();
Assert.AreEqual(0, process.ExitCode, "The dotnet/msbuild subprocess invocation exited with non-zero exitcode.");
}
}
/// <summary>
/// Compare the JSON results to the expected results
/// </summary>
/// <param name="expected"></param>
/// <param name="actual"></param>
private void CompareResults(Results expected, Results actual)
{
// Check set of .proto files processed is the same
var protofiles = expected.ProtoFiles;
CollectionAssert.AreEquivalent(protofiles, actual.ProtoFiles, "Set of .proto files being processed must match.");
// check protoc arguments
foreach (string protofile in protofiles)
{
var expectedArgs = expected.GetArgumentNames(protofile);
var actualArgs = actual.GetArgumentNames(protofile);
CollectionAssert.AreEquivalent(expectedArgs, actualArgs, $"Set of protoc arguments used for {protofile} must match.");
// Check the values.
// Any value with:
// - IGNORE: - will not be compared but must exist
// - REGEX: - compare using a regular expression
// - anything else is an exact match
// Expected results can also have tokens that are replaced before comparing:
// - ${TEST_OUT_DIR} - the test output directory
foreach (string argname in expectedArgs)
{
var expectedValues = expected.GetArgumentValues(protofile, argname);
var actualValues = actual.GetArgumentValues(protofile, argname);
Assert.AreEqual(expectedValues.Count, actualValues.Count,
$"{protofile}: Wrong number of occurrences of argument '{argname}'");
// Since generally the order of arguments on the commandline is important,
// it is fair to compare arguments with expected values one by one.
// Most arguments are only used at most once by the msbuild integration anyway.
for (int i = 0; i < expectedValues.Count; i++)
{
var expectedValue = ReplaceTokens(expectedValues[i]);
var actualValue = actualValues[i];
if (expectedValue.StartsWith("IGNORE:"))
continue;
var regexPrefix = "REGEX:";
if (expectedValue.StartsWith(regexPrefix))
{
string pattern = expectedValue.Substring(regexPrefix.Length);
Assert.IsTrue(Regex.IsMatch(actualValue, pattern),
$"{protofile}: Expected value '{expectedValue}' for argument '{argname}'. Actual value: '{actualValue}'");
}
else
{
Assert.AreEqual(expectedValue, actualValue, $"{protofile}: Wrong value for argument '{argname}'");
}
}
}
}
}
private string ReplaceTokens(string original)
{
return original
.Replace("${TEST_OUT_DIR}", testOutDir);
}
/// <summary>
/// Helper class for formatting the string specifying the list of proto files and
/// the expected generated files for each proto file.
/// </summary>
public class ExpectedFilesBuilder
{
private readonly List<string> protoAndFiles = new List<string>();
public ExpectedFilesBuilder Add(string protoFile, params string[] files)
{
protoAndFiles.Add(protoFile + ":" + string.Join(";", files));
return this;
}
public override string ToString()
{
return string.Join("|", protoAndFiles.ToArray());
}
}
/// <summary>
/// Hold the JSON results
/// </summary>
public class Results
{
/// <summary>
/// JSON "Metadata"
/// </summary>
public Dictionary<string, string> Metadata { get; set; }
/// <summary>
/// JSON "Files"
/// </summary>
public Dictionary<string, Dictionary<string, List<string>>> Files { get; set; }
/// <summary>
/// Read a JSON file
/// </summary>
/// <param name="filepath"></param>
/// <returns></returns>
public static Results Read(string filepath)
{
using (StreamReader file = File.OpenText(filepath))
{
JsonSerializer serializer = new JsonSerializer();
Results results = (Results)serializer.Deserialize(file, typeof(Results));
return results;
}
}
/// <summary>
/// Get the proto file names from the JSON
/// </summary>
public SortedSet<string> ProtoFiles => new SortedSet<string>(Files.Keys);
/// <summary>
/// Get the protoc arguments for the associated proto file
/// </summary>
/// <param name="protofile"></param>
/// <returns></returns>
public SortedSet<string> GetArgumentNames(string protofile)
{
Dictionary<string, List<string>> args;
if (Files.TryGetValue(protofile, out args))
{
return new SortedSet<string>(args.Keys);
}
else
{
return new SortedSet<string>();
}
}
/// <summary>
/// Get the values for the named argument for the proto file
/// </summary>
/// <param name="protofile">proto file</param>
/// <param name="name">argument</param>
/// <returns></returns>
public List<string> GetArgumentValues(string protofile, string name)
{
Dictionary<string, List<string>> args;
if (Files.TryGetValue(protofile, out args))
{
List<string> values;
if (args.TryGetValue(name, out values))
{
return new List<string>(values);
}
}
return new List<string>();
}
}
}
}

@ -0,0 +1,18 @@
@rem Copyright 2022 The gRPC Authors
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@echo off
set script_dir=%~dp0.
python.exe "%script_dir%\fakeprotoc.py" %*
exit /B %errorlevel%

@ -0,0 +1,353 @@
#!/usr/bin/env python3
# Copyright 2022 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.
# Fake protobuf compiler for use in the Grpc.Tools MSBuild integration
# unit tests. Its purpose is to be called from the Grpc.Tools
# Google.Protobuf.Tools.targets MSBuild file instead of the actual protoc
# compiler. This script:
# - parses the command line arguments
# - generates expected dependencies file
# - generates dummy .cs files that are expected by the tests
# - writes a JSON results file containing the arguments passed in
# Configuration is done via environment variables as it is not possible
# to pass additional argument when called from the MSBuild scripts under test.
#
# Environment variables:
# FAKEPROTOC_PROJECTDIR - project directory
# FAKEPROTOC_OUTDIR - output directory for generated files and output file
# FAKEPROTOC_GENERATE_EXPECTED - list of expected generated files in format:
# file1.proto:csfile1.cs;csfile2.cs|file2.proto:csfile3.cs;csfile4.cs|...
import datetime
import hashlib
import json
import os
import sys
# Set to True to write out debug messages from this script
_dbg = True
# file to which write the debug log
_dbgfile = None
def _open_debug_log(filename):
"""Create debug file for this script."""
global _dbgfile
if _dbg:
# append mode since this script may be called multiple times
# during one build/test
_dbgfile = open(filename, "a")
def _close_debug_log():
"""Close the debug log file."""
if _dbgfile:
_dbgfile.close()
def _write_debug(msg):
"""Write to the debug log file if debug is enabled."""
if _dbg and _dbgfile:
print(msg, file=_dbgfile, flush=True)
def _read_protoc_arguments():
"""
Get the protoc argument from the command line and
any response files specified on the command line.
Returns the list of arguments.
"""
_write_debug("\nread_protoc_arguments")
result = []
for arg in sys.argv[1:]:
_write_debug(" arg: " + arg)
if arg.startswith("@"):
# TODO(jtattermusch): inserting a "commented out" argument feels hacky
result.append("# RSP file: %s" % arg)
rsp_file_name = arg[1:]
result.extend(_read_rsp_file(rsp_file_name))
else:
result.append(arg)
return result
def _read_rsp_file(rspfile):
"""
Returns list of arguments from a response file.
"""
_write_debug("\nread_rsp_file: " + rspfile)
result = []
with open(rspfile, "r") as rsp:
for line in rsp:
line = line.strip()
_write_debug(" line: " + line)
result.append(line)
return result
def _parse_protoc_arguments(protoc_args, projectdir):
"""
Parse the protoc arguments from the provided list
"""
_write_debug("\nparse_protoc_arguments")
arg_dict = {}
for arg in protoc_args:
_write_debug("Parsing: %s" % arg)
# All arguments containing file or directory paths are
# normalized by converting all '\' and changed to '/'
if arg.startswith("--"):
# Assumes that cmdline arguments are always passed in the
# "--somearg=argvalue", which happens to be the form that
# msbuild integration uses, but it's not the only way.
(name, value) = arg.split("=", 1)
if name == "--dependency_out" or name == "--grpc_out" or name == "--csharp_out":
# For args that contain a path, make the path absolute and normalize it
# to make it easier to assert equality in tests.
value = _normalized_absolute_path(value)
if name == "--proto_path":
# for simplicity keep this one as relative path rather than absolute path
# since it is an input file that is always be near the project file
value = _normalized_relative_to_projectdir(value, projectdir)
_add_protoc_arg_to_dict(arg_dict, name, value)
elif arg.startswith("#"):
pass # ignore
else:
# arg represents a proto file name
arg = _normalized_relative_to_projectdir(arg, projectdir)
_add_protoc_arg_to_dict(arg_dict, "protofile", arg)
return arg_dict
def _add_protoc_arg_to_dict(arg_dict, name, value):
"""
Add the arguments with name/value to a multi-dictionary of arguments
"""
if name not in arg_dict:
arg_dict[name] = []
arg_dict[name].append(value)
def _normalized_relative_to_projectdir(file, projectdir):
"""Convert a file path to one relative to the project directory."""
try:
return _normalize_slashes(
os.path.relpath(os.path.abspath(file), projectdir))
except ValueError:
# On Windows if the paths are on different drives then we get this error
# Just return the absolute path
return _normalize_slashes(os.path.abspath(file))
def _normalized_absolute_path(file):
"""Returns normalized absolute path to file."""
return _normalize_slashes(os.path.abspath(file))
def _normalize_slashes(path):
"""Change all backslashes to forward slashes to make comparing path strings easier."""
return path.replace("\\", "/")
def _write_or_update_results_json(log_dir, protofile, protoc_arg_dict):
""" Write or update the results JSON file """
# Read existing json.
# Since protoc may be called more than once each build/test if there is
# more than one protoc file, we read the existing data to add to it.
fname = os.path.abspath("%s/results.json" % log_dir)
if os.path.isfile(fname):
# Load the original contents.
with open(fname, "r") as forig:
results_json = json.load(forig)
else:
results_json = {}
results_json['Files'] = {}
results_json['Files'][protofile] = protoc_arg_dict
results_json["Metadata"] = {"timestamp": str(datetime.datetime.now())}
with open(fname, "w") as fout:
json.dump(results_json, fout, indent=4)
def _parse_generate_expected(generate_expected_str):
"""
Parse FAKEPROTOC_GENERATE_EXPECTED that specifies the proto files
and the cs files to generate. We rely on the test to say what is
expected rather than trying to work it out in this script.
The format of the input is:
file1.proto:csfile1.cs;csfile2.cs|file2.proto:csfile3.cs;csfile4.cs|...
"""
_write_debug("\nparse_generate_expected")
result = {}
entries = generate_expected_str.split("|")
for entry in entries:
parts = entry.split(":")
pfile = _normalize_slashes(parts[0])
csfiles = parts[1].split(";")
result[pfile] = csfiles
_write_debug(pfile + " : " + str(csfiles))
return result
def _get_cs_files_to_generate(protofile, proto_to_generated):
"""Returns list of .cs files to generated based on FAKEPROTOC_GENERATE_EXPECTED env."""
protoname_normalized = _normalize_slashes(protofile)
cs_files_to_generate = proto_to_generated.get(protoname_normalized)
return cs_files_to_generate
def _generate_cs_files(protofile, cs_files_to_generate, out_dir, projectdir):
"""Create expected cs files."""
_write_debug("\ngenerate_cs_files")
if not cs_files_to_generate:
_write_debug("No .cs files matching proto file name %s" % protofile)
return
if not os.path.isabs(out_dir):
# if not absolute, it is relative to project directory
out_dir = os.path.abspath("%s/%s" % (projectdir, out_dir))
# Ensure out_dir exists
if not os.path.isdir(out_dir):
os.makedirs(out_dir)
timestamp = str(datetime.datetime.now())
for csfile in cs_files_to_generate:
csfile_fullpath = "%s/%s" % (out_dir, csfile)
_write_debug("Creating: %s" % csfile_fullpath)
with open(csfile_fullpath, "w") as fout:
print("// Generated by fake protoc: %s" % timestamp, file=fout)
def _create_dependency_file(protofile, cs_files_to_generate, dependencyfile,
grpcout):
"""Create the expected dependency file."""
_write_debug("\ncreate_dependency_file")
if not dependencyfile:
_write_debug("dependencyfile is not set.")
return
if not cs_files_to_generate:
_write_debug("No .cs files matching proto file name %s" % protofile)
return
_write_debug("Creating dependency file: %s" % dependencyfile)
with open(dependencyfile, "w") as out:
nfiles = len(cs_files_to_generate)
for i in range(0, nfiles):
cs_filename = os.path.join(grpcout, cs_files_to_generate[i])
if i == nfiles - 1:
print("%s: %s" % (cs_filename, protofile), file=out)
else:
print("%s \\" % cs_filename, file=out)
def _getenv(name):
# Note there is a bug in .NET core 3.x that lowercases the environment
# variable names when they are added via Process.StartInfo, so we need to
# check both cases here (only an issue on Linux which is case sensitive)
value = os.getenv(name)
if value is None:
value = os.getenv(name.lower())
return value
def main():
# Check environment variables for the additional arguments used in the tests.
projectdir = _getenv('FAKEPROTOC_PROJECTDIR')
if not projectdir:
print("FAKEPROTOC_PROJECTDIR not set")
sys.exit(1)
projectdir = os.path.abspath(projectdir)
# Output directory for generated files and output file
protoc_outdir = _getenv('FAKEPROTOC_OUTDIR')
if not protoc_outdir:
print("FAKEPROTOC_OUTDIR not set")
sys.exit(1)
protoc_outdir = os.path.abspath(protoc_outdir)
# Get list of expected generated files from env variable
generate_expected = _getenv('FAKEPROTOC_GENERATE_EXPECTED')
if not generate_expected:
print("FAKEPROTOC_GENERATE_EXPECTED not set")
sys.exit(1)
# Prepare the debug log
log_dir = os.path.join(protoc_outdir, "log")
if not os.path.isdir(log_dir):
os.makedirs(log_dir)
_open_debug_log("%s/fakeprotoc_log.txt" % log_dir)
_write_debug(
("##### fakeprotoc called at %s\n" + "FAKEPROTOC_PROJECTDIR = %s\n" +
"FAKEPROTOC_GENERATE_EXPECTED = %s\n") %
(datetime.datetime.now(), projectdir, generate_expected))
proto_to_generated = _parse_generate_expected(generate_expected)
protoc_args = _read_protoc_arguments()
protoc_arg_dict = _parse_protoc_arguments(protoc_args, projectdir)
# If argument was passed multiple times, take the last occurrence of it.
# TODO(jtattermusch): handle multiple occurrences of the same argument
dependencyfile = protoc_arg_dict.get('--dependency_out')[-1]
grpcout = protoc_arg_dict.get('--grpc_out')[-1]
if len(protoc_arg_dict.get('protofile')) != 1:
# regular protoc can process multiple .proto files passed at once, but we know
# the Grpc.Tools msbuild integration only ever passes one .proto file per invocation.
print(
"Expecting to get exactly one .proto file argument per fakeprotoc invocation."
)
sys.exit(1)
protofile = protoc_arg_dict.get('protofile')[0]
cs_files_to_generate = _get_cs_files_to_generate(
protofile=protofile, proto_to_generated=proto_to_generated)
_create_dependency_file(protofile=protofile,
cs_files_to_generate=cs_files_to_generate,
dependencyfile=dependencyfile,
grpcout=grpcout)
_generate_cs_files(protofile=protofile,
cs_files_to_generate=cs_files_to_generate,
out_dir=grpcout,
projectdir=projectdir)
_write_or_update_results_json(log_dir=log_dir,
protofile=protofile,
protoc_arg_dict=protoc_arg_dict)
_close_debug_log()
if __name__ == "__main__":
main()

@ -6,8 +6,9 @@
<Protobuf_Generator Condition=" '$(Protobuf_Generator)' == '' and '$(Language)' == 'C#' ">CSharp</Protobuf_Generator>
<!-- Configuration is passing the smoke test. -->
<Protobuf_ProjectSupported Condition=" '$(Protobuf_Generator)' != '' ">true</Protobuf_ProjectSupported>
<_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' == 'Core' ">netstandard1.3\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
<_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net45\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
<!-- Allow assembly file to be specified externally for testing by setting property _Protobuf_MsBuildAssembly -->
<_Protobuf_MsBuildAssembly Condition=" '$(_Protobuf_MsBuildAssembly)' == '' and '$(MSBuildRuntimeType)' == 'Core' ">netstandard1.3\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
<_Protobuf_MsBuildAssembly Condition=" '$(_Protobuf_MsBuildAssembly)' == '' and '$(MSBuildRuntimeType)' != 'Core' ">net45\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
</PropertyGroup>
<UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoToolsPlatform" />

@ -7,6 +7,7 @@
"Grpc.Tools.Tests.ProtoCompileBasicTest",
"Grpc.Tools.Tests.ProtoCompileCommandLineGeneratorTest",
"Grpc.Tools.Tests.ProtoCompileCommandLinePrinterTest",
"Grpc.Tools.Tests.ProtoToolsPlatformTaskTest"
"Grpc.Tools.Tests.ProtoToolsPlatformTaskTest",
"Grpc.Tools.Tests.MsBuildIntegrationTest"
]
}

Loading…
Cancel
Save