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