diff --git a/src/dotnet-grpc/Commands/AddFileCommand.cs b/src/dotnet-grpc/Commands/AddFileCommand.cs index 35302c143..01c1c9fe9 100644 --- a/src/dotnet-grpc/Commands/AddFileCommand.cs +++ b/src/dotnet-grpc/Commands/AddFileCommand.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -19,6 +19,7 @@ using System; using System.CommandLine; using System.CommandLine.Invocation; +using System.CommandLine.Parsing; using Grpc.Dotnet.Cli.Internal; using Grpc.Dotnet.Cli.Options; using Grpc.Dotnet.Cli.Properties; @@ -41,6 +42,7 @@ public static Command Create(HttpClient httpClient) var serviceOption = CommonOptions.ServiceOption(); var additionalImportDirsOption = CommonOptions.AdditionalImportDirsOption(); var accessOption = CommonOptions.AccessOption(); + var recursiveOption = CommonOptions.RecursiveOption(); var filesArgument = new Argument { Name = "files", @@ -52,6 +54,7 @@ public static Command Create(HttpClient httpClient) command.AddOption(serviceOption); command.AddOption(accessOption); command.AddOption(additionalImportDirsOption); + command.AddOption(recursiveOption); command.AddArgument(filesArgument); command.SetHandler( @@ -61,12 +64,13 @@ public static Command Create(HttpClient httpClient) var services = context.ParseResult.GetValueForOption(serviceOption); var access = context.ParseResult.GetValueForOption(accessOption); var additionalImportDirs = context.ParseResult.GetValueForOption(additionalImportDirsOption); + var searchOption = context.ParseResult.HasOption(recursiveOption) ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var files = context.ParseResult.GetValueForArgument(filesArgument); try { var command = new AddFileCommand(context.Console, project, httpClient); - await command.AddFileAsync(services, access, additionalImportDirs, files); + await command.AddFileAsync(services, access, additionalImportDirs, files, searchOption); context.ExitCode = 0; } @@ -81,11 +85,11 @@ public static Command Create(HttpClient httpClient) return command; } - public async Task AddFileAsync(Services services, Access access, string? additionalImportDirs, string[] files) + public async Task AddFileAsync(Services services, Access access, string? additionalImportDirs, string[] files, SearchOption searchOption) { var resolvedServices = ResolveServices(services); await EnsureNugetPackagesAsync(resolvedServices); - files = GlobReferences(files); + files = GlobReferences(files, searchOption); foreach (var file in files) { diff --git a/src/dotnet-grpc/Commands/CommandBase.cs b/src/dotnet-grpc/Commands/CommandBase.cs index d0a400cfd..41f4cd4ee 100644 --- a/src/dotnet-grpc/Commands/CommandBase.cs +++ b/src/dotnet-grpc/Commands/CommandBase.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -174,7 +174,7 @@ public void AddProtobufReference(Services services, string? additionalImportDirs } var normalizedFile = NormalizePath(file); - + var normalizedAdditionalImportDirs = string.Empty; if (!string.IsNullOrWhiteSpace(additionalImportDirs)) @@ -298,7 +298,7 @@ public IEnumerable ResolveReferences(string[] references) return resolvedReferences; } - internal string[] GlobReferences(string[] references) + internal string[] GlobReferences(string[] references, SearchOption searchOption = SearchOption.TopDirectoryOnly) { var expandedReferences = new List(); @@ -315,7 +315,7 @@ internal string[] GlobReferences(string[] references) var directoryToSearch = Path.GetPathRoot(reference)!; var searchPattern = reference.Substring(directoryToSearch.Length); - var resolvedFiles = Directory.GetFiles(directoryToSearch, searchPattern); + var resolvedFiles = Directory.GetFiles(directoryToSearch, searchPattern, searchOption); if (resolvedFiles.Length == 0) { @@ -328,7 +328,7 @@ internal string[] GlobReferences(string[] references) if (Directory.Exists(Path.Combine(Project.DirectoryPath, Path.GetDirectoryName(reference)!))) { - var resolvedFiles = Directory.GetFiles(Project.DirectoryPath, reference); + var resolvedFiles = Directory.GetFiles(Project.DirectoryPath, reference, searchOption); if (resolvedFiles.Length == 0) { diff --git a/src/dotnet-grpc/Options/CommonOptions.cs b/src/dotnet-grpc/Options/CommonOptions.cs index 0a5f4e3f9..667961301 100644 --- a/src/dotnet-grpc/Options/CommonOptions.cs +++ b/src/dotnet-grpc/Options/CommonOptions.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -54,4 +54,13 @@ public static Option AdditionalImportDirsOption() description: CoreStrings.AdditionalImportDirsOption); return o; } + + public static Option RecursiveOption() + { + var o = new Option( + aliases: new[] { "-r", "--recursive" }, + description: CoreStrings.RecursiveOptionDescription + ); + return o; + } } diff --git a/src/dotnet-grpc/Properties/CoreStrings.Designer.cs b/src/dotnet-grpc/Properties/CoreStrings.Designer.cs index c2b852195..83c419fe1 100644 --- a/src/dotnet-grpc/Properties/CoreStrings.Designer.cs +++ b/src/dotnet-grpc/Properties/CoreStrings.Designer.cs @@ -19,7 +19,7 @@ namespace Grpc.Dotnet.Cli.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class CoreStrings { @@ -312,6 +312,15 @@ internal static string ProjectOptionDescription { } } + /// + /// Looks up a localized string similar to Whether to add the protobuf file references recursively. Default value is false.. + /// + internal static string RecursiveOptionDescription { + get { + return ResourceManager.GetString("RecursiveOptionDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to The URL(s) or file path(s) to remote protobuf references(s) that should be updated. Leave this argument empty to refresh all remote references.. /// diff --git a/src/dotnet-grpc/Properties/CoreStrings.resx b/src/dotnet-grpc/Properties/CoreStrings.resx index 2a7b16be0..496f02a42 100644 --- a/src/dotnet-grpc/Properties/CoreStrings.resx +++ b/src/dotnet-grpc/Properties/CoreStrings.resx @@ -231,4 +231,7 @@ No protobuf references in the gRPC project. + + Whether to add the protobuf file references recursively. Default value is false. + \ No newline at end of file diff --git a/test/dotnet-grpc.Tests/AddFileCommandTests.cs b/test/dotnet-grpc.Tests/AddFileCommandTests.cs index eb75e9d98..9bf327397 100644 --- a/test/dotnet-grpc.Tests/AddFileCommandTests.cs +++ b/test/dotnet-grpc.Tests/AddFileCommandTests.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -41,7 +41,7 @@ public async Task Commandline_AddFileCommand_AddsPackagesAndReferences() var parser = Program.BuildParser(CreateClient()); // Act - var result = await parser.InvokeAsync($"add-file -p {tempDir} -s Server --access Internal -i ImportDir {Path.Combine("Proto", "*.proto")}", testConsole); + var result = await parser.InvokeAsync($"add-file -p {tempDir} -s Server --access Internal -r -i ImportDir {Path.Combine("Proto", "*.proto")}", testConsole); // Assert Assert.AreEqual(0, result, testConsole.Error.ToString()); @@ -55,9 +55,10 @@ public async Task Commandline_AddFileCommand_AddsPackagesAndReferences() var protoRefs = project.GetItems(CommandBase.ProtobufElement); - Assert.AreEqual(2, protoRefs.Count); + Assert.AreEqual(3, protoRefs.Count); Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\a.proto")); Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\b.proto")); + Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\Subfolder\\c.proto")); foreach (var protoRef in protoRefs) { Assert.AreEqual("Server", protoRef.GetMetadataValue(CommandBase.GrpcServicesElement)); @@ -81,7 +82,7 @@ public async Task AddFileCommand_AddsPackagesAndReferences() // Act Directory.SetCurrentDirectory(tempDir); var command = new AddFileCommand(new TestConsole(), projectPath: null, CreateClient()); - await command.AddFileAsync(Services.Server, Access.Internal, "ImportDir", new[] { Path.Combine("Proto", "*.proto") }); + await command.AddFileAsync(Services.Server, Access.Internal, "ImportDir", new[] { Path.Combine("Proto", "*.proto") }, SearchOption.TopDirectoryOnly); command.Project.ReevaluateIfNecessary(); // Assert @@ -89,11 +90,48 @@ public async Task AddFileCommand_AddsPackagesAndReferences() Assert.AreEqual(1, packageRefs.Count); Assert.NotNull(packageRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Grpc.AspNetCore" && !r.HasMetadata(CommandBase.PrivateAssetsElement))); - var protoRefs = command.Project.GetItems(CommandBase.ProtobufElement); Assert.AreEqual(2, protoRefs.Count); Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\a.proto")); Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\b.proto")); + Assert.Null(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\Subfolder\\c.proto")); + foreach (var protoRef in protoRefs) + { + Assert.AreEqual("Server", protoRef.GetMetadataValue(CommandBase.GrpcServicesElement)); + Assert.AreEqual("ImportDir", protoRef.GetMetadataValue(CommandBase.AdditionalImportDirsElement)); + Assert.AreEqual("Internal", protoRef.GetMetadataValue(CommandBase.AccessElement)); + } + + // Cleanup + Directory.SetCurrentDirectory(currentDir); + Directory.Delete(tempDir, true); + } + + [Test] + [NonParallelizable] + public async Task AddFileCommand_AddsPackagesAndReferencesRecursively() + { + // Arrange + var currentDir = Directory.GetCurrentDirectory(); + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + new DirectoryInfo(Path.Combine(currentDir, "TestAssets", "EmptyProject")).CopyTo(tempDir); + + // Act + Directory.SetCurrentDirectory(tempDir); + var command = new AddFileCommand(new TestConsole(), projectPath: null, CreateClient()); + await command.AddFileAsync(Services.Server, Access.Internal, "ImportDir", new[] { Path.Combine("Proto", "*.proto") }, SearchOption.AllDirectories); + command.Project.ReevaluateIfNecessary(); + + // Assert + var packageRefs = command.Project.GetItems(CommandBase.PackageReferenceElement); + Assert.AreEqual(1, packageRefs.Count); + Assert.NotNull(packageRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Grpc.AspNetCore" && !r.HasMetadata(CommandBase.PrivateAssetsElement))); + + var protoRefs = command.Project.GetItems(CommandBase.ProtobufElement); + Assert.AreEqual(3, protoRefs.Count); + Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\a.proto")); + Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\b.proto")); + Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\Subfolder\\c.proto")); foreach (var protoRef in protoRefs) { Assert.AreEqual("Server", protoRef.GetMetadataValue(CommandBase.GrpcServicesElement)); diff --git a/test/dotnet-grpc.Tests/TestAssets/EmptyProject/Proto/Subfolder/c.proto b/test/dotnet-grpc.Tests/TestAssets/EmptyProject/Proto/Subfolder/c.proto new file mode 100644 index 000000000..e69de29bb diff --git a/test/dotnet-grpc.Tests/dotnet-grpc.Tests.csproj b/test/dotnet-grpc.Tests/dotnet-grpc.Tests.csproj index bd90e57ce..b45313825 100644 --- a/test/dotnet-grpc.Tests/dotnet-grpc.Tests.csproj +++ b/test/dotnet-grpc.Tests/dotnet-grpc.Tests.csproj @@ -59,5 +59,8 @@ + + +