1 #region Copyright notice and license 2 3 // Copyright 2018 gRPC authors. 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 #endregion 18 19 using System.IO; 20 using Microsoft.Build.Framework; 21 using Moq; 22 using NUnit.Framework; 23 24 namespace Grpc.Tools.Tests 25 { 26 public class ProtoCompileCommandLineGeneratorTest : ProtoCompileBasicTest 27 { 28 [SetUp] SetUp()29 public new void SetUp() 30 { 31 _task.Generator = "csharp"; 32 _task.OutputDir = "outdir"; 33 _task.Protobuf = Utils.MakeSimpleItems("a.proto"); 34 } 35 ExecuteExpectSuccess()36 void ExecuteExpectSuccess() 37 { 38 _mockEngine 39 .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) 40 .Callback((BuildErrorEventArgs e) => 41 Assert.Fail($"Error logged by build engine:\n{e.Message}")); 42 bool result = _task.Execute(); 43 Assert.IsTrue(result); 44 } 45 46 [Test] MinimalCompile()47 public void MinimalCompile() 48 { 49 ExecuteExpectSuccess(); 50 Assert.That(_task.LastPathToTool, Does.Match(@"protoc(.exe)?$")); 51 Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { 52 "--csharp_out=outdir", "--error_format=msvs", "a.proto" })); 53 } 54 55 [Test] CompileTwoFiles()56 public void CompileTwoFiles() 57 { 58 _task.Protobuf = Utils.MakeSimpleItems("a.proto", "foo/b.proto"); 59 ExecuteExpectSuccess(); 60 Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { 61 "--csharp_out=outdir", "--error_format=msvs", "a.proto", "foo/b.proto" })); 62 } 63 64 [Test] CompileWithProtoPaths()65 public void CompileWithProtoPaths() 66 { 67 _task.ProtoPath = new[] { "/path1", "/path2" }; 68 ExecuteExpectSuccess(); 69 Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { 70 "--csharp_out=outdir", "--proto_path=/path1", 71 "--proto_path=/path2", "--error_format=msvs", "a.proto" })); 72 } 73 74 [TestCase("Cpp")] 75 [TestCase("CSharp")] 76 [TestCase("Java")] 77 [TestCase("Javanano")] 78 [TestCase("Js")] 79 [TestCase("Objc")] 80 [TestCase("Php")] 81 [TestCase("Python")] 82 [TestCase("Ruby")] CompileWithOptions(string gen)83 public void CompileWithOptions(string gen) 84 { 85 _task.Generator = gen; 86 _task.OutputOptions = new[] { "foo", "bar" }; 87 ExecuteExpectSuccess(); 88 gen = gen.ToLowerInvariant(); 89 Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { 90 $"--{gen}_out=outdir", $"--{gen}_opt=foo,bar", "--error_format=msvs", "a.proto" })); 91 } 92 93 [Test] OutputDependencyFile()94 public void OutputDependencyFile() 95 { 96 _task.DependencyOut = "foo/my.protodep"; 97 // Task fails trying to read the non-generated file; we ignore that. 98 _task.Execute(); 99 Assert.That(_task.LastResponseFile, 100 Does.Contain("--dependency_out=foo/my.protodep")); 101 } 102 103 [Test] OutputDependencyWithProtoDepDir()104 public void OutputDependencyWithProtoDepDir() 105 { 106 _task.ProtoDepDir = "foo"; 107 // Task fails trying to read the non-generated file; we ignore that. 108 _task.Execute(); 109 Assert.That(_task.LastResponseFile, 110 Has.One.Match(@"^--dependency_out=foo[/\\].+_a.protodep$")); 111 } 112 113 [Test] GenerateGrpc()114 public void GenerateGrpc() 115 { 116 _task.GrpcPluginExe = "/foo/grpcgen"; 117 ExecuteExpectSuccess(); 118 Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { 119 "--csharp_out=outdir", "--grpc_out=outdir", 120 "--plugin=protoc-gen-grpc=/foo/grpcgen" })); 121 } 122 123 [Test] GenerateGrpcWithOutDir()124 public void GenerateGrpcWithOutDir() 125 { 126 _task.GrpcPluginExe = "/foo/grpcgen"; 127 _task.GrpcOutputDir = "gen-out"; 128 ExecuteExpectSuccess(); 129 Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { 130 "--csharp_out=outdir", "--grpc_out=gen-out" })); 131 } 132 133 [Test] GenerateGrpcWithOptions()134 public void GenerateGrpcWithOptions() 135 { 136 _task.GrpcPluginExe = "/foo/grpcgen"; 137 _task.GrpcOutputOptions = new[] { "baz", "quux" }; 138 ExecuteExpectSuccess(); 139 Assert.That(_task.LastResponseFile, 140 Does.Contain("--grpc_opt=baz,quux")); 141 } 142 143 [Test] AdditionalProtocArguments()144 public void AdditionalProtocArguments() 145 { 146 _task.AdditionalProtocArguments = new[] { "--experimental_allow_proto3_optional" }; 147 ExecuteExpectSuccess(); 148 Assert.That(_task.LastResponseFile, 149 Does.Contain("--experimental_allow_proto3_optional")); 150 } 151 152 [Test] DirectoryArgumentsSlashTrimmed()153 public void DirectoryArgumentsSlashTrimmed() 154 { 155 _task.GrpcPluginExe = "/foo/grpcgen"; 156 _task.GrpcOutputDir = "gen-out/"; 157 _task.OutputDir = "outdir/"; 158 _task.ProtoPath = new[] { "/path1/", "/path2/" }; 159 ExecuteExpectSuccess(); 160 Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { 161 "--proto_path=/path1", "--proto_path=/path2", 162 "--csharp_out=outdir", "--grpc_out=gen-out" })); 163 } 164 165 [TestCase(".", ".")] 166 [TestCase("/", "/")] 167 [TestCase("//", "/")] 168 [TestCase("/foo/", "/foo")] 169 [TestCase("/foo", "/foo")] 170 [TestCase("foo/", "foo")] 171 [TestCase("foo//", "foo")] 172 [TestCase("foo/\\", "foo")] 173 [TestCase("foo\\/", "foo")] 174 [TestCase("C:\\foo", "C:\\foo")] 175 [TestCase("C:", "C:")] 176 [TestCase("C:\\", "C:\\")] 177 [TestCase("C:\\\\", "C:\\")] DirectorySlashTrimmingCases(string given, string expect)178 public void DirectorySlashTrimmingCases(string given, string expect) 179 { 180 if (Path.DirectorySeparatorChar == '/') 181 expect = expect.Replace('\\', '/'); 182 _task.OutputDir = given; 183 ExecuteExpectSuccess(); 184 Assert.That(_task.LastResponseFile, 185 Does.Contain("--csharp_out=" + expect)); 186 } 187 188 [TestCase( 189 "../Protos/greet.proto(19) : warning in column=5 : warning : When enum name is stripped and label is PascalCased (Zero) this value label conflicts with Zero.", 190 "../Protos/greet.proto", 191 19, 192 5, 193 "warning : When enum name is stripped and label is PascalCased (Zero) this value label conflicts with Zero.")] 194 [TestCase( 195 "../Protos/greet.proto: warning: Import google/protobuf/empty.proto but not used.", 196 "../Protos/greet.proto", 197 0, 198 0, 199 "Import google/protobuf/empty.proto but not used.")] 200 [TestCase("../Protos/greet.proto(14) : error in column=10: \"name\" is already defined in \"Greet.HelloRequest\".", null, 0, 0, null)] 201 [TestCase("../Protos/greet.proto: Import \"google / protobuf / empty.proto\" was listed twice.", null, 0, 0, null)] 202 [TestCase("[libprotobuf WARNING T:\\altsrc\\github\\...\\csharp\\csharp_enum.cc:74] Duplicate enum value Work (originally Work) in PhoneType; adding underscore to distinguish", 203 "T:\\altsrc\\github\\...\\csharp\\csharp_enum.cc", 204 74, 0, 205 "Duplicate enum value Work (originally Work) in PhoneType; adding underscore to distinguish")] 206 [TestCase("[libprotobuf ERROR T:\\path\\...\\filename:23] Some message", null, 0, 0, null)] 207 [TestCase("[libprotobuf FATAL T:\\path\\...\\filename:23] Some message", null, 0, 0, null)] WarningsParsed(string stderr, string file, int line, int col, string message)208 public void WarningsParsed(string stderr, string file, int line, int col, string message) 209 { 210 _task.StdErrMessages.Add(stderr); 211 212 bool matched = false; 213 _mockEngine 214 .Setup(me => me.LogWarningEvent(It.IsAny<BuildWarningEventArgs>())) 215 .Callback((BuildWarningEventArgs e) => { 216 matched = true; 217 if (file != null) 218 { 219 Assert.AreEqual(file, e.File); 220 Assert.AreEqual(line, e.LineNumber); 221 Assert.AreEqual(col, e.ColumnNumber); 222 Assert.AreEqual(message, e.Message); 223 } 224 else 225 { 226 Assert.Fail($"Error logged by build engine:\n{e.Message}"); 227 } 228 }); 229 230 bool result = _task.Execute(); 231 Assert.IsFalse(result); 232 233 // To get here in the test then either the event fired and the values matched 234 // or the event did not fire (input did not parse as a warning). 235 // If it did not parse as a warning then check that the expected message is null 236 if (!matched && message != null) 237 { 238 Assert.Fail($"Expected match: {message}"); 239 } 240 } 241 242 [TestCase( 243 "../Protos/greet.proto(14) : error in column=10: \"name\" is already defined in \"Greet.HelloRequest\".", 244 "../Protos/greet.proto", 245 14, 246 10, 247 "\"name\" is already defined in \"Greet.HelloRequest\".")] 248 [TestCase( 249 "../Protos/greet.proto: Import \"google / protobuf / empty.proto\" was listed twice.", 250 "../Protos/greet.proto", 251 0, 252 0, 253 "Import \"google / protobuf / empty.proto\" was listed twice.")] 254 [TestCase("../Protos/greet.proto(19) : warning in column=5 : warning : When enum name is stripped and label is PascalCased (Zero) this value label conflicts with Zero.", null, 0, 0, null)] 255 [TestCase("../Protos/greet.proto: warning: Import google/protobuf/empty.proto but not used.", null, 0, 0, null)] 256 [TestCase("[libprotobuf WARNING T:\\altsrc\\github\\...\\csharp\\csharp_enum.cc:74] Duplicate enum value Work (originally Work) in PhoneType; adding underscore to distinguish", 257 null, 0, 0, null)] 258 [TestCase("[libprotobuf ERROR T:\\path\\...\\filename:23] Some message", 259 "T:\\path\\...\\filename", 23, 0, "ERROR Some message")] 260 [TestCase("[libprotobuf FATAL T:\\path\\...\\filename:23] Some message", 261 "T:\\path\\...\\filename", 23, 0, "FATAL Some message")] ErrorsParsed(string stderr, string file, int line, int col, string message)262 public void ErrorsParsed(string stderr, string file, int line, int col, string message) 263 { 264 _task.StdErrMessages.Add(stderr); 265 266 bool matched = false; 267 _mockEngine 268 .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) 269 .Callback((BuildErrorEventArgs e) => { 270 matched = true; 271 if (file != null) 272 { 273 Assert.AreEqual(file, e.File); 274 Assert.AreEqual(line, e.LineNumber); 275 Assert.AreEqual(col, e.ColumnNumber); 276 Assert.AreEqual(message, e.Message); 277 } 278 else 279 { 280 // Ignore expected error 281 // "protoc/protoc.exe" existed with code -1. 282 if (!e.Message.EndsWith("exited with code -1.")) 283 { 284 Assert.Fail($"Error logged by build engine:\n{e.Message}"); 285 } 286 } 287 }); 288 289 290 bool result = _task.Execute(); 291 Assert.IsFalse(result); 292 293 // To get here in the test then either the event fired and the values matched 294 // or the event did not fire (input did not parse as an error). 295 // If it did not parse as an error then check that the expected message is null 296 if (!matched && message != null) 297 { 298 Assert.Fail($"Expected match: {message}"); 299 } 300 } 301 }; 302 } 303