xref: /aosp_15_r20/external/grpc-grpc/src/csharp/Grpc.Tools.Tests/MsBuildIntegrationTest.cs (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1*cc02d7e2SAndroid Build Coastguard Worker #region Copyright notice and license
2*cc02d7e2SAndroid Build Coastguard Worker 
3*cc02d7e2SAndroid Build Coastguard Worker // Copyright 2022 The gRPC Authors
4*cc02d7e2SAndroid Build Coastguard Worker //
5*cc02d7e2SAndroid Build Coastguard Worker // Licensed under the Apache License, Version 2.0 (the "License");
6*cc02d7e2SAndroid Build Coastguard Worker // you may not use this file except in compliance with the License.
7*cc02d7e2SAndroid Build Coastguard Worker // You may obtain a copy of the License at
8*cc02d7e2SAndroid Build Coastguard Worker //
9*cc02d7e2SAndroid Build Coastguard Worker //     http://www.apache.org/licenses/LICENSE-2.0
10*cc02d7e2SAndroid Build Coastguard Worker //
11*cc02d7e2SAndroid Build Coastguard Worker // Unless required by applicable law or agreed to in writing, software
12*cc02d7e2SAndroid Build Coastguard Worker // distributed under the License is distributed on an "AS IS" BASIS,
13*cc02d7e2SAndroid Build Coastguard Worker // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*cc02d7e2SAndroid Build Coastguard Worker // See the License for the specific language governing permissions and
15*cc02d7e2SAndroid Build Coastguard Worker // limitations under the License.
16*cc02d7e2SAndroid Build Coastguard Worker 
17*cc02d7e2SAndroid Build Coastguard Worker #endregion
18*cc02d7e2SAndroid Build Coastguard Worker 
19*cc02d7e2SAndroid Build Coastguard Worker using System;
20*cc02d7e2SAndroid Build Coastguard Worker using System.IO;
21*cc02d7e2SAndroid Build Coastguard Worker using NUnit.Framework;
22*cc02d7e2SAndroid Build Coastguard Worker using System.Diagnostics;
23*cc02d7e2SAndroid Build Coastguard Worker using System.Reflection;
24*cc02d7e2SAndroid Build Coastguard Worker using System.Collections.Specialized;
25*cc02d7e2SAndroid Build Coastguard Worker using System.Collections;
26*cc02d7e2SAndroid Build Coastguard Worker using System.Collections.Generic;
27*cc02d7e2SAndroid Build Coastguard Worker using System.Text.RegularExpressions;
28*cc02d7e2SAndroid Build Coastguard Worker using Newtonsoft.Json;
29*cc02d7e2SAndroid Build Coastguard Worker 
30*cc02d7e2SAndroid Build Coastguard Worker namespace Grpc.Tools.Tests
31*cc02d7e2SAndroid Build Coastguard Worker {
32*cc02d7e2SAndroid Build Coastguard Worker     /// <summary>
33*cc02d7e2SAndroid Build Coastguard Worker     /// Tests for Grpc.Tools MSBuild .target and .props files.
34*cc02d7e2SAndroid Build Coastguard Worker     /// </summary>
35*cc02d7e2SAndroid Build Coastguard Worker     /// <remarks>
36*cc02d7e2SAndroid Build Coastguard Worker     /// The Grpc.Tools NuGet package is not tested directly, but instead the
37*cc02d7e2SAndroid Build Coastguard Worker     /// same .target and .props files are included in a MSBuild project and
38*cc02d7e2SAndroid Build Coastguard Worker     /// that project is built using "dotnet build" with the SDK installed on
39*cc02d7e2SAndroid Build Coastguard Worker     /// the test machine.
40*cc02d7e2SAndroid Build Coastguard Worker     /// <para>
41*cc02d7e2SAndroid Build Coastguard Worker     /// The real protoc compiler is not called. Instead a fake protoc script is
42*cc02d7e2SAndroid Build Coastguard Worker     /// called that does the minimum work needed for the build to succeed
43*cc02d7e2SAndroid Build Coastguard Worker     /// (generating cs files and writing dependencies file) and also writes out
44*cc02d7e2SAndroid Build Coastguard Worker     /// the arguments it was called with in a JSON file. The output is checked
45*cc02d7e2SAndroid Build Coastguard Worker     /// with expected results.
46*cc02d7e2SAndroid Build Coastguard Worker     /// </para>
47*cc02d7e2SAndroid Build Coastguard Worker     /// </remarks>
48*cc02d7e2SAndroid Build Coastguard Worker     [TestFixture]
49*cc02d7e2SAndroid Build Coastguard Worker     public class MsBuildIntegrationTest
50*cc02d7e2SAndroid Build Coastguard Worker     {
51*cc02d7e2SAndroid Build Coastguard Worker         private const string TASKS_ASSEMBLY_PROPERTY = "_Protobuf_MsBuildAssembly";
52*cc02d7e2SAndroid Build Coastguard Worker         private const string TASKS_ASSEMBLY_DLL = "Protobuf.MSBuild.dll";
53*cc02d7e2SAndroid Build Coastguard Worker         private const string PROTBUF_FULLPATH_PROPERTY = "Protobuf_ProtocFullPath";
54*cc02d7e2SAndroid Build Coastguard Worker         private const string PLUGIN_FULLPATH_PROPERTY = "gRPC_PluginFullPath";
55*cc02d7e2SAndroid Build Coastguard Worker         private const string TOOLS_BUILD_DIR_PROPERTY = "GrpcToolsBuildDir";
56*cc02d7e2SAndroid Build Coastguard Worker 
57*cc02d7e2SAndroid Build Coastguard Worker         private const string MSBUILD_LOG_VERBOSITY = "diagnostic"; // "diagnostic" or "detailed"
58*cc02d7e2SAndroid Build Coastguard Worker 
59*cc02d7e2SAndroid Build Coastguard Worker         private string testId;
60*cc02d7e2SAndroid Build Coastguard Worker         private string fakeProtoc;
61*cc02d7e2SAndroid Build Coastguard Worker         private string grpcToolsBuildDir;
62*cc02d7e2SAndroid Build Coastguard Worker         private string tasksAssembly;
63*cc02d7e2SAndroid Build Coastguard Worker         private string testDataDir;
64*cc02d7e2SAndroid Build Coastguard Worker         private string testProjectDir;
65*cc02d7e2SAndroid Build Coastguard Worker         private string testOutBaseDir;
66*cc02d7e2SAndroid Build Coastguard Worker         private string testOutDir;
67*cc02d7e2SAndroid Build Coastguard Worker 
68*cc02d7e2SAndroid Build Coastguard Worker         [SetUp]
InitTest()69*cc02d7e2SAndroid Build Coastguard Worker         public void InitTest()
70*cc02d7e2SAndroid Build Coastguard Worker         {
71*cc02d7e2SAndroid Build Coastguard Worker #if NET45
72*cc02d7e2SAndroid Build Coastguard Worker             // We need to run these tests for one framework.
73*cc02d7e2SAndroid Build Coastguard Worker             // This test class is just a driver for calling the
74*cc02d7e2SAndroid Build Coastguard Worker             // "dotnet build" processes, so it doesn't matter what
75*cc02d7e2SAndroid Build Coastguard Worker             // the runtime of this class actually is.
76*cc02d7e2SAndroid Build Coastguard Worker             Assert.Ignore("Skipping test when NET45");
77*cc02d7e2SAndroid Build Coastguard Worker #endif
78*cc02d7e2SAndroid Build Coastguard Worker         }
79*cc02d7e2SAndroid Build Coastguard Worker 
80*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestSingleProto()81*cc02d7e2SAndroid Build Coastguard Worker         public void TestSingleProto()
82*cc02d7e2SAndroid Build Coastguard Worker         {
83*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestSingleProto));
84*cc02d7e2SAndroid Build Coastguard Worker 
85*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
86*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("file.proto", "File.cs", "FileGrpc.cs");
87*cc02d7e2SAndroid Build Coastguard Worker 
88*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestSingleProto", expectedFiles.ToString());
89*cc02d7e2SAndroid Build Coastguard Worker         }
90*cc02d7e2SAndroid Build Coastguard Worker 
91*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestMultipleProtos()92*cc02d7e2SAndroid Build Coastguard Worker         public void TestMultipleProtos()
93*cc02d7e2SAndroid Build Coastguard Worker         {
94*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestMultipleProtos));
95*cc02d7e2SAndroid Build Coastguard Worker 
96*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
97*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("file.proto", "File.cs", "FileGrpc.cs")
98*cc02d7e2SAndroid Build Coastguard Worker                 .Add("protos/another.proto", "Another.cs", "AnotherGrpc.cs")
99*cc02d7e2SAndroid Build Coastguard Worker                 .Add("second.proto", "Second.cs", "SecondGrpc.cs")
100*cc02d7e2SAndroid Build Coastguard Worker                 // Test duplicate name under different directories is allowed.
101*cc02d7e2SAndroid Build Coastguard Worker                 // See https://github.com/grpc/grpc/issues/17672
102*cc02d7e2SAndroid Build Coastguard Worker                 .Add("protos/file.proto", "File.cs", "FileGrpc.cs");
103*cc02d7e2SAndroid Build Coastguard Worker 
104*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestMultipleProtos", expectedFiles.ToString());
105*cc02d7e2SAndroid Build Coastguard Worker         }
106*cc02d7e2SAndroid Build Coastguard Worker 
107*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestAtInPath()108*cc02d7e2SAndroid Build Coastguard Worker         public void TestAtInPath()
109*cc02d7e2SAndroid Build Coastguard Worker         {
110*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestAtInPath));
111*cc02d7e2SAndroid Build Coastguard Worker 
112*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
113*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("@protos/file.proto", "File.cs", "FileGrpc.cs");
114*cc02d7e2SAndroid Build Coastguard Worker 
115*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestAtInPath", expectedFiles.ToString());
116*cc02d7e2SAndroid Build Coastguard Worker         }
117*cc02d7e2SAndroid Build Coastguard Worker 
118*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestProtoOutsideProject()119*cc02d7e2SAndroid Build Coastguard Worker         public void TestProtoOutsideProject()
120*cc02d7e2SAndroid Build Coastguard Worker         {
121*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestProtoOutsideProject), "TestProtoOutsideProject/project");
122*cc02d7e2SAndroid Build Coastguard Worker 
123*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
124*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("../api/greet.proto", "Greet.cs", "GreetGrpc.cs");
125*cc02d7e2SAndroid Build Coastguard Worker 
126*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestProtoOutsideProject/project", expectedFiles.ToString());
127*cc02d7e2SAndroid Build Coastguard Worker         }
128*cc02d7e2SAndroid Build Coastguard Worker 
129*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestCharactersInName()130*cc02d7e2SAndroid Build Coastguard Worker         public void TestCharactersInName()
131*cc02d7e2SAndroid Build Coastguard Worker         {
132*cc02d7e2SAndroid Build Coastguard Worker             // see https://github.com/grpc/grpc/issues/17661 - dot in name
133*cc02d7e2SAndroid Build Coastguard Worker             // and https://github.com/grpc/grpc/issues/18698 - numbers in name
134*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestCharactersInName));
135*cc02d7e2SAndroid Build Coastguard Worker 
136*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
137*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("protos/hello.world.proto", "HelloWorld.cs", "Hello.worldGrpc.cs");
138*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("protos/m_double_2d.proto", "MDouble2D.cs", "MDouble2dGrpc.cs");
139*cc02d7e2SAndroid Build Coastguard Worker 
140*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestCharactersInName", expectedFiles.ToString());
141*cc02d7e2SAndroid Build Coastguard Worker         }
142*cc02d7e2SAndroid Build Coastguard Worker 
143*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestExtraOptions()144*cc02d7e2SAndroid Build Coastguard Worker         public void TestExtraOptions()
145*cc02d7e2SAndroid Build Coastguard Worker         {
146*cc02d7e2SAndroid Build Coastguard Worker             // Test various extra options passed to protoc and plugin
147*cc02d7e2SAndroid Build Coastguard Worker             // See https://github.com/grpc/grpc/issues/25950
148*cc02d7e2SAndroid Build Coastguard Worker             // Tests setting AdditionalProtocArguments, OutputOptions and GrpcOutputOptions
149*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestExtraOptions));
150*cc02d7e2SAndroid Build Coastguard Worker 
151*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
152*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("file.proto", "File.cs", "FileGrpc.cs");
153*cc02d7e2SAndroid Build Coastguard Worker 
154*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestExtraOptions", expectedFiles.ToString());
155*cc02d7e2SAndroid Build Coastguard Worker         }
156*cc02d7e2SAndroid Build Coastguard Worker 
157*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestGrpcServicesMetadata()158*cc02d7e2SAndroid Build Coastguard Worker         public void TestGrpcServicesMetadata()
159*cc02d7e2SAndroid Build Coastguard Worker         {
160*cc02d7e2SAndroid Build Coastguard Worker             // Test different values for GrpcServices item metadata
161*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestGrpcServicesMetadata));
162*cc02d7e2SAndroid Build Coastguard Worker 
163*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
164*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("messages.proto", "Messages.cs");
165*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("serveronly.proto", "Serveronly.cs", "ServeronlyGrpc.cs");
166*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("clientonly.proto", "Clientonly.cs", "ClientonlyGrpc.cs");
167*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("clientandserver.proto", "Clientandserver.cs", "ClientandserverGrpc.cs");
168*cc02d7e2SAndroid Build Coastguard Worker 
169*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestGrpcServicesMetadata", expectedFiles.ToString());
170*cc02d7e2SAndroid Build Coastguard Worker         }
171*cc02d7e2SAndroid Build Coastguard Worker 
172*cc02d7e2SAndroid Build Coastguard Worker         [Test]
TestSetOutputDirs()173*cc02d7e2SAndroid Build Coastguard Worker         public void TestSetOutputDirs()
174*cc02d7e2SAndroid Build Coastguard Worker         {
175*cc02d7e2SAndroid Build Coastguard Worker             // Test setting different GrpcOutputDir and OutputDir
176*cc02d7e2SAndroid Build Coastguard Worker             SetUpForTest(nameof(TestSetOutputDirs));
177*cc02d7e2SAndroid Build Coastguard Worker 
178*cc02d7e2SAndroid Build Coastguard Worker             var expectedFiles = new ExpectedFilesBuilder();
179*cc02d7e2SAndroid Build Coastguard Worker             expectedFiles.Add("file.proto", "File.cs", "FileGrpc.cs");
180*cc02d7e2SAndroid Build Coastguard Worker 
181*cc02d7e2SAndroid Build Coastguard Worker             TryRunMsBuild("TestSetOutputDirs", expectedFiles.ToString());
182*cc02d7e2SAndroid Build Coastguard Worker         }
183*cc02d7e2SAndroid Build Coastguard Worker 
184*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
185*cc02d7e2SAndroid Build Coastguard Worker         /// Set up common paths for all the tests
186*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
SetUpCommonPaths()187*cc02d7e2SAndroid Build Coastguard Worker         private void SetUpCommonPaths()
188*cc02d7e2SAndroid Build Coastguard Worker         {
189*cc02d7e2SAndroid Build Coastguard Worker             var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
190*cc02d7e2SAndroid Build Coastguard Worker             testDataDir = Path.GetFullPath($"{assemblyDir}/../../../IntegrationTests");
191*cc02d7e2SAndroid Build Coastguard Worker 
192*cc02d7e2SAndroid Build Coastguard Worker             // Path for fake proto.
193*cc02d7e2SAndroid Build Coastguard Worker             // On Windows we have to wrap the python script in a BAT script since we can only
194*cc02d7e2SAndroid Build Coastguard Worker             // pass one executable name without parameters to the MSBuild
195*cc02d7e2SAndroid Build Coastguard Worker             // - e.g. we can't give "python fakeprotoc.py"
196*cc02d7e2SAndroid Build Coastguard Worker             var fakeProtocScript = Platform.IsWindows ? "fakeprotoc.bat" : "fakeprotoc.py";
197*cc02d7e2SAndroid Build Coastguard Worker             fakeProtoc = Path.GetFullPath($"{assemblyDir}/../../../scripts/{fakeProtocScript}");
198*cc02d7e2SAndroid Build Coastguard Worker 
199*cc02d7e2SAndroid Build Coastguard Worker             // Path for "build" directory under Grpc.Tools
200*cc02d7e2SAndroid Build Coastguard Worker             grpcToolsBuildDir = Path.GetFullPath($"{assemblyDir}/../../../../Grpc.Tools/build");
201*cc02d7e2SAndroid Build Coastguard Worker 
202*cc02d7e2SAndroid Build Coastguard Worker             // Task assembly is needed to run the extension tasks
203*cc02d7e2SAndroid Build Coastguard Worker             // We use the assembly that was copied next to Grpc.Tools.Tests.dll
204*cc02d7e2SAndroid Build Coastguard Worker             // as a Grpc.Tools.Tests dependency since we know it's the correct one
205*cc02d7e2SAndroid Build Coastguard Worker             // and we don't have to figure out its original path (which is different
206*cc02d7e2SAndroid Build Coastguard Worker             // for debug/release builds etc).
207*cc02d7e2SAndroid Build Coastguard Worker             tasksAssembly = Path.Combine(assemblyDir, TASKS_ASSEMBLY_DLL);
208*cc02d7e2SAndroid Build Coastguard Worker 
209*cc02d7e2SAndroid Build Coastguard Worker             // put test ouptput directory outside of Grpc.Tools.Tests to avoid problems with
210*cc02d7e2SAndroid Build Coastguard Worker             // repeated builds.
211*cc02d7e2SAndroid Build Coastguard Worker             testOutBaseDir = NormalizePath(Path.GetFullPath($"{assemblyDir}/../../../../test-out/grpc_tools_integration_tests"));
212*cc02d7e2SAndroid Build Coastguard Worker         }
213*cc02d7e2SAndroid Build Coastguard Worker 
214*cc02d7e2SAndroid Build Coastguard Worker 
215*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
216*cc02d7e2SAndroid Build Coastguard Worker         /// Normalize path string to use just forward slashes. That makes it easier to compare paths
217*cc02d7e2SAndroid Build Coastguard Worker         /// for equality in the tests.
218*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
NormalizePath(string path)219*cc02d7e2SAndroid Build Coastguard Worker         private string NormalizePath(string path)
220*cc02d7e2SAndroid Build Coastguard Worker         {
221*cc02d7e2SAndroid Build Coastguard Worker             return path.Replace('\\','/');
222*cc02d7e2SAndroid Build Coastguard Worker         }
223*cc02d7e2SAndroid Build Coastguard Worker 
224*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
225*cc02d7e2SAndroid Build Coastguard Worker         /// Set up test specific paths
226*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
227*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="testName">Name of the test</param>
228*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="testPath">Optional path to the test project</param>
SetUpForTest(string testName, string testPath = null)229*cc02d7e2SAndroid Build Coastguard Worker         private void SetUpForTest(string testName, string testPath = null)
230*cc02d7e2SAndroid Build Coastguard Worker         {
231*cc02d7e2SAndroid Build Coastguard Worker             if (testPath == null) {
232*cc02d7e2SAndroid Build Coastguard Worker                 testPath = testName;
233*cc02d7e2SAndroid Build Coastguard Worker             }
234*cc02d7e2SAndroid Build Coastguard Worker 
235*cc02d7e2SAndroid Build Coastguard Worker             SetUpCommonPaths();
236*cc02d7e2SAndroid Build Coastguard Worker 
237*cc02d7e2SAndroid Build Coastguard Worker             testId = $"{testName}_run-{Guid.NewGuid().ToString()}";
238*cc02d7e2SAndroid Build Coastguard Worker             Console.WriteLine($"TestID for test: {testId}");
239*cc02d7e2SAndroid Build Coastguard Worker 
240*cc02d7e2SAndroid Build Coastguard Worker             // Paths for test data
241*cc02d7e2SAndroid Build Coastguard Worker             testProjectDir = NormalizePath(Path.Combine(testDataDir, testPath));
242*cc02d7e2SAndroid Build Coastguard Worker             testOutDir = NormalizePath(Path.Combine(testOutBaseDir, testId));
243*cc02d7e2SAndroid Build Coastguard Worker         }
244*cc02d7e2SAndroid Build Coastguard Worker 
245*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
246*cc02d7e2SAndroid Build Coastguard Worker         /// Run "dotnet build" on the test's project file.
247*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
248*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="testName">Name of test and name of directory containing the test</param>
249*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="filesToGenerate">Tell the fake protoc script which files to generate</param>
250*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="testId">A unique ID for the test run - used to create results file</param>
TryRunMsBuild(string testName, string filesToGenerate)251*cc02d7e2SAndroid Build Coastguard Worker         private void TryRunMsBuild(string testName, string filesToGenerate)
252*cc02d7e2SAndroid Build Coastguard Worker         {
253*cc02d7e2SAndroid Build Coastguard Worker             Directory.CreateDirectory(testOutDir);
254*cc02d7e2SAndroid Build Coastguard Worker 
255*cc02d7e2SAndroid Build Coastguard Worker             // create the arguments for the "dotnet build"
256*cc02d7e2SAndroid Build Coastguard Worker             var args = $"build -p:{TASKS_ASSEMBLY_PROPERTY}={tasksAssembly}"
257*cc02d7e2SAndroid Build Coastguard Worker                 + $" -p:TestOutDir={testOutDir}"
258*cc02d7e2SAndroid Build Coastguard Worker                 + $" -p:BaseOutputPath={testOutDir}/bin/"
259*cc02d7e2SAndroid Build Coastguard Worker                 + $" -p:BaseIntermediateOutputPath={testOutDir}/obj/"
260*cc02d7e2SAndroid Build Coastguard Worker                 + $" -p:{TOOLS_BUILD_DIR_PROPERTY}={grpcToolsBuildDir}"
261*cc02d7e2SAndroid Build Coastguard Worker                 + $" -p:{PROTBUF_FULLPATH_PROPERTY}={fakeProtoc}"
262*cc02d7e2SAndroid Build Coastguard Worker                 + $" -p:{PLUGIN_FULLPATH_PROPERTY}=dummy-plugin-not-used"
263*cc02d7e2SAndroid Build Coastguard Worker                 + $" -fl -flp:LogFile={testOutDir}/log/msbuild.log;verbosity={MSBUILD_LOG_VERBOSITY}"
264*cc02d7e2SAndroid Build Coastguard Worker                 + $" msbuildtest.csproj";
265*cc02d7e2SAndroid Build Coastguard Worker 
266*cc02d7e2SAndroid Build Coastguard Worker             // To pass additional parameters to fake protoc process
267*cc02d7e2SAndroid Build Coastguard Worker             // we need to use environment variables
268*cc02d7e2SAndroid Build Coastguard Worker             var envVariables = new StringDictionary {
269*cc02d7e2SAndroid Build Coastguard Worker                 { "FAKEPROTOC_PROJECTDIR", testProjectDir },
270*cc02d7e2SAndroid Build Coastguard Worker                 { "FAKEPROTOC_OUTDIR", testOutDir },
271*cc02d7e2SAndroid Build Coastguard Worker                 { "FAKEPROTOC_GENERATE_EXPECTED", filesToGenerate },
272*cc02d7e2SAndroid Build Coastguard Worker                 { "FAKEPROTOC_TESTID", testId }
273*cc02d7e2SAndroid Build Coastguard Worker             };
274*cc02d7e2SAndroid Build Coastguard Worker 
275*cc02d7e2SAndroid Build Coastguard Worker             // Run the "dotnet build"
276*cc02d7e2SAndroid Build Coastguard Worker             ProcessMsbuild(args, testProjectDir, envVariables);
277*cc02d7e2SAndroid Build Coastguard Worker 
278*cc02d7e2SAndroid Build Coastguard Worker             // Check the results JSON matches the expected JSON
279*cc02d7e2SAndroid Build Coastguard Worker             Results actualResults = Results.Read(testOutDir + "/log/results.json");
280*cc02d7e2SAndroid Build Coastguard Worker             Results expectedResults = Results.Read(testProjectDir + "/expected.json");
281*cc02d7e2SAndroid Build Coastguard Worker             CompareResults(expectedResults, actualResults);
282*cc02d7e2SAndroid Build Coastguard Worker         }
283*cc02d7e2SAndroid Build Coastguard Worker 
284*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
285*cc02d7e2SAndroid Build Coastguard Worker         /// Run the "dotnet build" command
286*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
287*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="args">arguments to the dotnet command</param>
288*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="workingDirectory">working directory</param>
289*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="envVariables">environment variables to set</param>
ProcessMsbuild(string args, string workingDirectory, StringDictionary envVariables)290*cc02d7e2SAndroid Build Coastguard Worker         private void ProcessMsbuild(string args, string workingDirectory, StringDictionary envVariables)
291*cc02d7e2SAndroid Build Coastguard Worker         {
292*cc02d7e2SAndroid Build Coastguard Worker             using (var process = new Process())
293*cc02d7e2SAndroid Build Coastguard Worker             {
294*cc02d7e2SAndroid Build Coastguard Worker                 process.StartInfo.FileName = "dotnet";
295*cc02d7e2SAndroid Build Coastguard Worker                 process.StartInfo.Arguments = args;
296*cc02d7e2SAndroid Build Coastguard Worker                 process.StartInfo.RedirectStandardOutput = true;
297*cc02d7e2SAndroid Build Coastguard Worker                 process.StartInfo.RedirectStandardError = true;
298*cc02d7e2SAndroid Build Coastguard Worker                 process.StartInfo.WorkingDirectory = workingDirectory;
299*cc02d7e2SAndroid Build Coastguard Worker                 process.StartInfo.UseShellExecute = false;
300*cc02d7e2SAndroid Build Coastguard Worker                 StringDictionary procEnv = process.StartInfo.EnvironmentVariables;
301*cc02d7e2SAndroid Build Coastguard Worker                 foreach (DictionaryEntry entry in envVariables)
302*cc02d7e2SAndroid Build Coastguard Worker                 {
303*cc02d7e2SAndroid Build Coastguard Worker                     if (!procEnv.ContainsKey((string)entry.Key))
304*cc02d7e2SAndroid Build Coastguard Worker                     {
305*cc02d7e2SAndroid Build Coastguard Worker                         procEnv.Add((string)entry.Key, (string)entry.Value);
306*cc02d7e2SAndroid Build Coastguard Worker                     }
307*cc02d7e2SAndroid Build Coastguard Worker                 }
308*cc02d7e2SAndroid Build Coastguard Worker 
309*cc02d7e2SAndroid Build Coastguard Worker                 process.OutputDataReceived += (sender, e) => {
310*cc02d7e2SAndroid Build Coastguard Worker                     if (e.Data != null)
311*cc02d7e2SAndroid Build Coastguard Worker                     {
312*cc02d7e2SAndroid Build Coastguard Worker                         Console.WriteLine(e.Data);
313*cc02d7e2SAndroid Build Coastguard Worker                     }
314*cc02d7e2SAndroid Build Coastguard Worker                 };
315*cc02d7e2SAndroid Build Coastguard Worker                 process.ErrorDataReceived += (sender, e) => {
316*cc02d7e2SAndroid Build Coastguard Worker                     if (e.Data != null)
317*cc02d7e2SAndroid Build Coastguard Worker                     {
318*cc02d7e2SAndroid Build Coastguard Worker                         Console.WriteLine(e.Data);
319*cc02d7e2SAndroid Build Coastguard Worker                     }
320*cc02d7e2SAndroid Build Coastguard Worker                 };
321*cc02d7e2SAndroid Build Coastguard Worker 
322*cc02d7e2SAndroid Build Coastguard Worker                 process.Start();
323*cc02d7e2SAndroid Build Coastguard Worker 
324*cc02d7e2SAndroid Build Coastguard Worker                 process.BeginErrorReadLine();
325*cc02d7e2SAndroid Build Coastguard Worker                 process.BeginOutputReadLine();
326*cc02d7e2SAndroid Build Coastguard Worker 
327*cc02d7e2SAndroid Build Coastguard Worker                 process.WaitForExit();
328*cc02d7e2SAndroid Build Coastguard Worker                 Assert.AreEqual(0, process.ExitCode, "The dotnet/msbuild subprocess invocation exited with non-zero exitcode.");
329*cc02d7e2SAndroid Build Coastguard Worker             }
330*cc02d7e2SAndroid Build Coastguard Worker         }
331*cc02d7e2SAndroid Build Coastguard Worker 
332*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
333*cc02d7e2SAndroid Build Coastguard Worker         /// Compare the JSON results to the expected results
334*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
335*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="expected"></param>
336*cc02d7e2SAndroid Build Coastguard Worker         /// <param name="actual"></param>
CompareResults(Results expected, Results actual)337*cc02d7e2SAndroid Build Coastguard Worker         private void CompareResults(Results expected, Results actual)
338*cc02d7e2SAndroid Build Coastguard Worker         {
339*cc02d7e2SAndroid Build Coastguard Worker             // Check set of .proto files processed is the same
340*cc02d7e2SAndroid Build Coastguard Worker             var protofiles = expected.ProtoFiles;
341*cc02d7e2SAndroid Build Coastguard Worker             CollectionAssert.AreEquivalent(protofiles, actual.ProtoFiles, "Set of .proto files being processed must match.");
342*cc02d7e2SAndroid Build Coastguard Worker 
343*cc02d7e2SAndroid Build Coastguard Worker             // check protoc arguments
344*cc02d7e2SAndroid Build Coastguard Worker             foreach (string protofile in protofiles)
345*cc02d7e2SAndroid Build Coastguard Worker             {
346*cc02d7e2SAndroid Build Coastguard Worker                 var expectedArgs = expected.GetArgumentNames(protofile);
347*cc02d7e2SAndroid Build Coastguard Worker                 var actualArgs = actual.GetArgumentNames(protofile);
348*cc02d7e2SAndroid Build Coastguard Worker                 CollectionAssert.AreEquivalent(expectedArgs, actualArgs, $"Set of protoc arguments used for {protofile} must match.");
349*cc02d7e2SAndroid Build Coastguard Worker 
350*cc02d7e2SAndroid Build Coastguard Worker                 // Check the values.
351*cc02d7e2SAndroid Build Coastguard Worker                 // Any value with:
352*cc02d7e2SAndroid Build Coastguard Worker                 // - IGNORE: - will not be compared but must exist
353*cc02d7e2SAndroid Build Coastguard Worker                 // - REGEX: - compare using a regular expression
354*cc02d7e2SAndroid Build Coastguard Worker                 // - anything else is an exact match
355*cc02d7e2SAndroid Build Coastguard Worker                 // Expected results can also have tokens that are replaced before comparing:
356*cc02d7e2SAndroid Build Coastguard Worker                 // - ${TEST_OUT_DIR} - the test output directory
357*cc02d7e2SAndroid Build Coastguard Worker                 foreach (string argname in expectedArgs)
358*cc02d7e2SAndroid Build Coastguard Worker                 {
359*cc02d7e2SAndroid Build Coastguard Worker                     var expectedValues = expected.GetArgumentValues(protofile, argname);
360*cc02d7e2SAndroid Build Coastguard Worker                     var actualValues = actual.GetArgumentValues(protofile, argname);
361*cc02d7e2SAndroid Build Coastguard Worker 
362*cc02d7e2SAndroid Build Coastguard Worker                     Assert.AreEqual(expectedValues.Count, actualValues.Count,
363*cc02d7e2SAndroid Build Coastguard Worker                                  $"{protofile}: Wrong number of occurrences of argument '{argname}'");
364*cc02d7e2SAndroid Build Coastguard Worker 
365*cc02d7e2SAndroid Build Coastguard Worker                     // Since generally the order of arguments on the commandline is important,
366*cc02d7e2SAndroid Build Coastguard Worker                     // it is fair to compare arguments with expected values one by one.
367*cc02d7e2SAndroid Build Coastguard Worker                     // Most arguments are only used at most once by the msbuild integration anyway.
368*cc02d7e2SAndroid Build Coastguard Worker                     for (int i = 0; i < expectedValues.Count; i++)
369*cc02d7e2SAndroid Build Coastguard Worker                     {
370*cc02d7e2SAndroid Build Coastguard Worker                         var expectedValue = ReplaceTokens(expectedValues[i]);
371*cc02d7e2SAndroid Build Coastguard Worker                         var actualValue = actualValues[i];
372*cc02d7e2SAndroid Build Coastguard Worker 
373*cc02d7e2SAndroid Build Coastguard Worker                         if (expectedValue.StartsWith("IGNORE:"))
374*cc02d7e2SAndroid Build Coastguard Worker                             continue;
375*cc02d7e2SAndroid Build Coastguard Worker 
376*cc02d7e2SAndroid Build Coastguard Worker                         var regexPrefix = "REGEX:";
377*cc02d7e2SAndroid Build Coastguard Worker                         if (expectedValue.StartsWith(regexPrefix))
378*cc02d7e2SAndroid Build Coastguard Worker                         {
379*cc02d7e2SAndroid Build Coastguard Worker                             string pattern = expectedValue.Substring(regexPrefix.Length);
380*cc02d7e2SAndroid Build Coastguard Worker                             Assert.IsTrue(Regex.IsMatch(actualValue, pattern),
381*cc02d7e2SAndroid Build Coastguard Worker                                  $"{protofile}: Expected value '{expectedValue}' for argument '{argname}'. Actual value: '{actualValue}'");
382*cc02d7e2SAndroid Build Coastguard Worker                         }
383*cc02d7e2SAndroid Build Coastguard Worker                         else
384*cc02d7e2SAndroid Build Coastguard Worker                         {
385*cc02d7e2SAndroid Build Coastguard Worker                             Assert.AreEqual(expectedValue, actualValue, $"{protofile}: Wrong value for argument '{argname}'");
386*cc02d7e2SAndroid Build Coastguard Worker                         }
387*cc02d7e2SAndroid Build Coastguard Worker                     }
388*cc02d7e2SAndroid Build Coastguard Worker                 }
389*cc02d7e2SAndroid Build Coastguard Worker             }
390*cc02d7e2SAndroid Build Coastguard Worker         }
391*cc02d7e2SAndroid Build Coastguard Worker 
ReplaceTokens(string original)392*cc02d7e2SAndroid Build Coastguard Worker         private string ReplaceTokens(string original)
393*cc02d7e2SAndroid Build Coastguard Worker         {
394*cc02d7e2SAndroid Build Coastguard Worker             return original
395*cc02d7e2SAndroid Build Coastguard Worker                 .Replace("${TEST_OUT_DIR}", testOutDir);
396*cc02d7e2SAndroid Build Coastguard Worker         }
397*cc02d7e2SAndroid Build Coastguard Worker 
398*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
399*cc02d7e2SAndroid Build Coastguard Worker         /// Helper class for formatting the string specifying the list of proto files and
400*cc02d7e2SAndroid Build Coastguard Worker         /// the expected generated files for each proto file.
401*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
402*cc02d7e2SAndroid Build Coastguard Worker         public class ExpectedFilesBuilder
403*cc02d7e2SAndroid Build Coastguard Worker         {
404*cc02d7e2SAndroid Build Coastguard Worker             private readonly List<string> protoAndFiles = new List<string>();
405*cc02d7e2SAndroid Build Coastguard Worker 
Add(string protoFile, params string[] files)406*cc02d7e2SAndroid Build Coastguard Worker             public ExpectedFilesBuilder Add(string protoFile, params string[] files)
407*cc02d7e2SAndroid Build Coastguard Worker             {
408*cc02d7e2SAndroid Build Coastguard Worker                 protoAndFiles.Add(protoFile + ":" + string.Join(";", files));
409*cc02d7e2SAndroid Build Coastguard Worker                 return this;
410*cc02d7e2SAndroid Build Coastguard Worker             }
411*cc02d7e2SAndroid Build Coastguard Worker 
ToString()412*cc02d7e2SAndroid Build Coastguard Worker             public override string ToString()
413*cc02d7e2SAndroid Build Coastguard Worker             {
414*cc02d7e2SAndroid Build Coastguard Worker                 return string.Join("|", protoAndFiles.ToArray());
415*cc02d7e2SAndroid Build Coastguard Worker             }
416*cc02d7e2SAndroid Build Coastguard Worker         }
417*cc02d7e2SAndroid Build Coastguard Worker 
418*cc02d7e2SAndroid Build Coastguard Worker         /// <summary>
419*cc02d7e2SAndroid Build Coastguard Worker         /// Hold the JSON results
420*cc02d7e2SAndroid Build Coastguard Worker         /// </summary>
421*cc02d7e2SAndroid Build Coastguard Worker         public class Results
422*cc02d7e2SAndroid Build Coastguard Worker         {
423*cc02d7e2SAndroid Build Coastguard Worker             /// <summary>
424*cc02d7e2SAndroid Build Coastguard Worker             /// JSON "Metadata"
425*cc02d7e2SAndroid Build Coastguard Worker             /// </summary>
426*cc02d7e2SAndroid Build Coastguard Worker             public Dictionary<string, string> Metadata { get; set; }
427*cc02d7e2SAndroid Build Coastguard Worker 
428*cc02d7e2SAndroid Build Coastguard Worker             /// <summary>
429*cc02d7e2SAndroid Build Coastguard Worker             /// JSON "Files"
430*cc02d7e2SAndroid Build Coastguard Worker             /// </summary>
431*cc02d7e2SAndroid Build Coastguard Worker             public Dictionary<string, Dictionary<string, List<string>>> Files { get; set; }
432*cc02d7e2SAndroid Build Coastguard Worker 
433*cc02d7e2SAndroid Build Coastguard Worker             /// <summary>
434*cc02d7e2SAndroid Build Coastguard Worker             /// Read a JSON file
435*cc02d7e2SAndroid Build Coastguard Worker             /// </summary>
436*cc02d7e2SAndroid Build Coastguard Worker             /// <param name="filepath"></param>
437*cc02d7e2SAndroid Build Coastguard Worker             /// <returns></returns>
Read(string filepath)438*cc02d7e2SAndroid Build Coastguard Worker             public static Results Read(string filepath)
439*cc02d7e2SAndroid Build Coastguard Worker             {
440*cc02d7e2SAndroid Build Coastguard Worker                 using (StreamReader file = File.OpenText(filepath))
441*cc02d7e2SAndroid Build Coastguard Worker                 {
442*cc02d7e2SAndroid Build Coastguard Worker                     JsonSerializer serializer = new JsonSerializer();
443*cc02d7e2SAndroid Build Coastguard Worker                     Results results = (Results)serializer.Deserialize(file, typeof(Results));
444*cc02d7e2SAndroid Build Coastguard Worker                     return results;
445*cc02d7e2SAndroid Build Coastguard Worker                 }
446*cc02d7e2SAndroid Build Coastguard Worker             }
447*cc02d7e2SAndroid Build Coastguard Worker 
448*cc02d7e2SAndroid Build Coastguard Worker             /// <summary>
449*cc02d7e2SAndroid Build Coastguard Worker             /// Get the proto file names from the JSON
450*cc02d7e2SAndroid Build Coastguard Worker             /// </summary>
451*cc02d7e2SAndroid Build Coastguard Worker             public SortedSet<string> ProtoFiles => new SortedSet<string>(Files.Keys);
452*cc02d7e2SAndroid Build Coastguard Worker 
453*cc02d7e2SAndroid Build Coastguard Worker             /// <summary>
454*cc02d7e2SAndroid Build Coastguard Worker             /// Get the protoc arguments for the associated proto file
455*cc02d7e2SAndroid Build Coastguard Worker             /// </summary>
456*cc02d7e2SAndroid Build Coastguard Worker             /// <param name="protofile"></param>
457*cc02d7e2SAndroid Build Coastguard Worker             /// <returns></returns>
GetArgumentNames(string protofile)458*cc02d7e2SAndroid Build Coastguard Worker             public SortedSet<string> GetArgumentNames(string protofile)
459*cc02d7e2SAndroid Build Coastguard Worker             {
460*cc02d7e2SAndroid Build Coastguard Worker                 Dictionary<string, List<string>> args;
461*cc02d7e2SAndroid Build Coastguard Worker                 if (Files.TryGetValue(protofile, out args))
462*cc02d7e2SAndroid Build Coastguard Worker                 {
463*cc02d7e2SAndroid Build Coastguard Worker                     return new SortedSet<string>(args.Keys);
464*cc02d7e2SAndroid Build Coastguard Worker                 }
465*cc02d7e2SAndroid Build Coastguard Worker                 else
466*cc02d7e2SAndroid Build Coastguard Worker                 {
467*cc02d7e2SAndroid Build Coastguard Worker                     return new SortedSet<string>();
468*cc02d7e2SAndroid Build Coastguard Worker                 }
469*cc02d7e2SAndroid Build Coastguard Worker             }
470*cc02d7e2SAndroid Build Coastguard Worker 
471*cc02d7e2SAndroid Build Coastguard Worker             /// <summary>
472*cc02d7e2SAndroid Build Coastguard Worker             /// Get the values for the named argument for the proto file
473*cc02d7e2SAndroid Build Coastguard Worker             /// </summary>
474*cc02d7e2SAndroid Build Coastguard Worker             /// <param name="protofile">proto file</param>
475*cc02d7e2SAndroid Build Coastguard Worker             /// <param name="name">argument</param>
476*cc02d7e2SAndroid Build Coastguard Worker             /// <returns></returns>
GetArgumentValues(string protofile, string name)477*cc02d7e2SAndroid Build Coastguard Worker             public List<string> GetArgumentValues(string protofile, string name)
478*cc02d7e2SAndroid Build Coastguard Worker             {
479*cc02d7e2SAndroid Build Coastguard Worker                 Dictionary<string, List<string>> args;
480*cc02d7e2SAndroid Build Coastguard Worker                 if (Files.TryGetValue(protofile, out args))
481*cc02d7e2SAndroid Build Coastguard Worker                 {
482*cc02d7e2SAndroid Build Coastguard Worker                     List<string> values;
483*cc02d7e2SAndroid Build Coastguard Worker                     if (args.TryGetValue(name, out values))
484*cc02d7e2SAndroid Build Coastguard Worker                     {
485*cc02d7e2SAndroid Build Coastguard Worker                         return new List<string>(values);
486*cc02d7e2SAndroid Build Coastguard Worker                     }
487*cc02d7e2SAndroid Build Coastguard Worker                 }
488*cc02d7e2SAndroid Build Coastguard Worker                 return new List<string>();
489*cc02d7e2SAndroid Build Coastguard Worker             }
490*cc02d7e2SAndroid Build Coastguard Worker         }
491*cc02d7e2SAndroid Build Coastguard Worker     }
492*cc02d7e2SAndroid Build Coastguard Worker 
493*cc02d7e2SAndroid Build Coastguard Worker 
494*cc02d7e2SAndroid Build Coastguard Worker }
495