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