Skip to content

Commit 770947b

Browse files
authored
Upgrade System.Commandline and add command line tests (#1700)
1 parent c9b8aa7 commit 770947b

File tree

18 files changed

+462
-152
lines changed

18 files changed

+462
-152
lines changed

build/dependencies.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
<Nunit3TestAdapterPackageVersion>3.16.1</Nunit3TestAdapterPackageVersion>
2424
<OpenTelemetryPackageVersion>0.6.0-beta.1</OpenTelemetryPackageVersion>
2525
<ProtobufNetGrpcPackageVersion>1.0.140</ProtobufNetGrpcPackageVersion>
26-
<SystemCommandLinePackageVersion>2.0.0-beta1.20214.1</SystemCommandLinePackageVersion>
27-
<SystemCommandLineRenderingPackageVersion>0.3.0-alpha.20214.1</SystemCommandLineRenderingPackageVersion>
26+
<SystemCommandLinePackageVersion>2.0.0-beta3.22114.1</SystemCommandLinePackageVersion>
27+
<SystemCommandLineRenderingPackageVersion>0.4.0-alpha.22114.1</SystemCommandLineRenderingPackageVersion>
2828
<SystemDiagnosticsDiagnosticSourcePackageVersion>4.5.1</SystemDiagnosticsDiagnosticSourcePackageVersion>
2929
<SystemIOPipelinesPackageVersion>5.0.1</SystemIOPipelinesPackageVersion>
3030
<SystemMemoryPackageVersion>4.5.3</SystemMemoryPackageVersion>

perf/benchmarkapps/GrpcClient/ClientOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace GrpcClient
2222
{
2323
public class ClientOptions
2424
{
25-
public string? Url { get; set; }
25+
public Uri? Url { get; set; }
2626
#if NET5_0_OR_GREATER
2727
public string? UdsFileName { get; set; }
2828
#endif

perf/benchmarkapps/GrpcClient/Program.cs

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#endregion
1818

1919
using System.CommandLine;
20+
using System.CommandLine.Binding;
2021
using System.CommandLine.Invocation;
2122
using System.Diagnostics;
2223
using System.Globalization;
@@ -58,29 +59,33 @@ class Program
5859

5960
public static async Task<int> Main(string[] args)
6061
{
62+
var options = new List<Option>();
63+
options.Add(new Option<Uri>(new string[] { "-u", "--url" }, "The server url to request") { IsRequired = true });
64+
options.Add(new Option<string>(new string[] { "--udsFileName" }, "The Unix Domain Socket file name"));
65+
options.Add(new Option<int>(new string[] { "-c", "--connections" }, () => 1, "Total number of connections to keep open"));
66+
options.Add(new Option<int>(new string[] { "-w", "--warmup" }, () => 5, "Duration of the warmup in seconds"));
67+
options.Add(new Option<int>(new string[] { "-d", "--duration" }, () => 10, "Duration of the test in seconds"));
68+
options.Add(new Option<int>(new string[] { "--callCount" }, "Call count of test"));
69+
options.Add(new Option<string>(new string[] { "-s", "--scenario" }, "Scenario to run") { IsRequired = true });
70+
options.Add(new Option<bool>(new string[] { "-l", "--latency" }, () => false, "Whether to collect detailed latency"));
71+
options.Add(new Option<string>(new string[] { "-p", "--protocol" }, "HTTP protocol") { IsRequired = true });
72+
options.Add(new Option<LogLevel>(new string[] { "-log", "--logLevel" }, () => LogLevel.None, "The log level to use for Console logging"));
73+
options.Add(new Option<int>(new string[] { "--requestSize" }, "Request payload size"));
74+
options.Add(new Option<int>(new string[] { "--responseSize" }, "Response payload size"));
75+
options.Add(new Option<GrpcClientType>(new string[] { "--grpcClientType" }, () => GrpcClientType.GrpcNetClient, "Whether to use Grpc.NetClient or Grpc.Core client"));
76+
options.Add(new Option<int>(new string[] { "--streams" }, () => 1, "Maximum concurrent streams per connection"));
77+
options.Add(new Option<bool>(new string[] { "--enableCertAuth" }, () => false, "Flag indicating whether client sends a client certificate"));
78+
options.Add(new Option<int>(new string[] { "--deadline" }, "Duration of deadline in seconds"));
79+
6180
var rootCommand = new RootCommand();
62-
rootCommand.AddOption(new Option<string>(new string[] { "-u", "--url" }, "The server url to request") { Required = true });
63-
rootCommand.AddOption(new Option<string>(new string[] { "--udsFileName" }, "The Unix Domain Socket file name"));
64-
rootCommand.AddOption(new Option<int>(new string[] { "-c", "--connections" }, () => 1, "Total number of connections to keep open"));
65-
rootCommand.AddOption(new Option<int>(new string[] { "-w", "--warmup" }, () => 5, "Duration of the warmup in seconds"));
66-
rootCommand.AddOption(new Option<int>(new string[] { "-d", "--duration" }, () => 10, "Duration of the test in seconds"));
67-
rootCommand.AddOption(new Option<int>(new string[] { "--callCount" }, "Call count of test"));
68-
rootCommand.AddOption(new Option<string>(new string[] { "-s", "--scenario" }, "Scenario to run") { Required = true });
69-
rootCommand.AddOption(new Option<bool>(new string[] { "-l", "--latency" }, () => false, "Whether to collect detailed latency"));
70-
rootCommand.AddOption(new Option<string>(new string[] { "-p", "--protocol" }, "HTTP protocol") { Required = true });
71-
rootCommand.AddOption(new Option<LogLevel>(new string[] { "-log", "--logLevel" }, () => LogLevel.None, "The log level to use for Console logging"));
72-
rootCommand.AddOption(new Option<int>(new string[] { "--requestSize" }, "Request payload size"));
73-
rootCommand.AddOption(new Option<int>(new string[] { "--responseSize" }, "Response payload size"));
74-
rootCommand.AddOption(new Option<GrpcClientType>(new string[] { "--grpcClientType" }, () => GrpcClientType.GrpcNetClient, "Whether to use Grpc.NetClient or Grpc.Core client"));
75-
rootCommand.AddOption(new Option<int>(new string[] { "--streams" }, () => 1, "Maximum concurrent streams per connection"));
76-
rootCommand.AddOption(new Option<bool>(new string[] { "--enableCertAuth" }, () => false, "Flag indicating whether client sends a client certificate"));
77-
rootCommand.AddOption(new Option<int>(new string[] { "--deadline" }, "Duration of deadline in seconds"));
78-
79-
rootCommand.Handler = CommandHandler.Create<ClientOptions>(async (options) =>
81+
foreach (var option in options)
8082
{
81-
_options = options;
83+
rootCommand.AddOption(option);
84+
}
8285

83-
Log("gRPC Client");
86+
rootCommand.SetHandler<ClientOptions>(async (options) =>
87+
{
88+
_options = options;
8489

8590
var runtimeVersion = typeof(object).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "Unknown";
8691
var isServerGC = GCSettings.IsServerGC;
@@ -103,11 +108,45 @@ public static async Task<int> Main(string[] args)
103108
await StartScenario();
104109

105110
await StopJobAsync();
106-
});
111+
}, new ReflectionBinder<ClientOptions>(options));
112+
113+
Log("gRPC Client");
107114

108115
return await rootCommand.InvokeAsync(args);
109116
}
110117

118+
private class ReflectionBinder<T> : BinderBase<T> where T : new()
119+
{
120+
private readonly List<Option> _options;
121+
122+
public ReflectionBinder(List<Option> options)
123+
{
124+
_options = options;
125+
}
126+
127+
protected override T GetBoundValue(BindingContext bindingContext)
128+
{
129+
var boundValue = new T();
130+
131+
Log($"Binding {typeof(T)}");
132+
133+
foreach (var option in _options)
134+
{
135+
var value = bindingContext.ParseResult.GetValueForOption(option);
136+
137+
var propertyInfo = typeof(T).GetProperty(option.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
138+
if (propertyInfo != null)
139+
{
140+
propertyInfo.SetValue(boundValue, value);
141+
142+
Log($"-{propertyInfo.Name} = {value}");
143+
}
144+
}
145+
146+
return boundValue;
147+
}
148+
}
149+
111150
private static async Task StartScenario()
112151
{
113152
if (_options.CallCount == null)
@@ -354,7 +393,7 @@ private static void CreateChannels()
354393
}
355394

356395
// Channel does not care about scheme
357-
var initialUri = new Uri(_options.Url!);
396+
var initialUri = _options.Url!;
358397
var resolvedUri = initialUri.Authority;
359398

360399
Log($"gRPC client type: {_options.GrpcClientType}");

src/dotnet-grpc/Commands/AddFileCommand.cs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,43 +27,49 @@ namespace Grpc.Dotnet.Cli.Commands
2727
{
2828
internal class AddFileCommand : CommandBase
2929
{
30-
public AddFileCommand(IConsole console, FileInfo? projectPath)
31-
: base(console, projectPath) { }
30+
public AddFileCommand(IConsole console, string? projectPath, HttpClient httpClient)
31+
: base(console, projectPath, httpClient) { }
3232

33-
public static Command Create()
33+
public static Command Create(HttpClient httpClient)
3434
{
3535
var command = new Command(
3636
name: "add-file",
3737
description: CoreStrings.AddFileCommandDescription);
38-
command.AddArgument(new Argument<string[]>
38+
39+
var projectOption = CommonOptions.ProjectOption();
40+
var serviceOption = CommonOptions.ServiceOption();
41+
var additionalImportDirsOption = CommonOptions.AdditionalImportDirsOption();
42+
var accessOption = CommonOptions.AccessOption();
43+
var filesArgument = new Argument<string[]>
3944
{
4045
Name = "files",
4146
Description = CoreStrings.AddFileCommandArgumentDescription,
4247
Arity = ArgumentArity.OneOrMore
43-
});
48+
};
4449

45-
command.AddOption(CommonOptions.ProjectOption());
46-
command.AddOption(CommonOptions.ServiceOption());
47-
command.AddOption(CommonOptions.AdditionalImportDirsOption());
48-
command.AddOption(CommonOptions.AccessOption());
50+
command.AddOption(projectOption);
51+
command.AddOption(serviceOption);
52+
command.AddOption(accessOption);
53+
command.AddOption(additionalImportDirsOption);
54+
command.AddArgument(filesArgument);
4955

50-
command.Handler = CommandHandler.Create<IConsole, FileInfo, Services, Access, string?, string[]>(
51-
async (console, project, services, access, additionalImportDirs, files) =>
56+
command.SetHandler<string, Services, Access, string?, string[], InvocationContext, IConsole>(
57+
async (project, services, access, additionalImportDirs, files, context, console) =>
5258
{
5359
try
5460
{
55-
var command = new AddFileCommand(console, project);
61+
var command = new AddFileCommand(console, project, httpClient);
5662
await command.AddFileAsync(services, access, additionalImportDirs, files);
5763

58-
return 0;
64+
context.ExitCode = 0;
5965
}
6066
catch (CLIToolException e)
6167
{
6268
console.LogError(e);
6369

64-
return -1;
70+
context.ExitCode = -1;
6571
}
66-
});
72+
}, projectOption, serviceOption, accessOption, additionalImportDirsOption, filesArgument);
6773

6874
return command;
6975
}

src/dotnet-grpc/Commands/AddUrlCommand.cs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,37 +26,42 @@ namespace Grpc.Dotnet.Cli.Commands
2626
{
2727
internal class AddUrlCommand : CommandBase
2828
{
29-
public AddUrlCommand(IConsole console, FileInfo? projectPath)
30-
: base(console, projectPath) { }
29+
public AddUrlCommand(IConsole console, string? projectPath, HttpClient httpClient)
30+
: base(console, projectPath, httpClient) { }
3131

3232
// Internal for testing
3333
internal AddUrlCommand(IConsole console, HttpClient client)
3434
: base(console, client) { }
3535

36-
public static Command Create()
36+
public static Command Create(HttpClient httpClient)
3737
{
3838
var command = new Command(
3939
name: "add-url",
4040
description: CoreStrings.AddUrlCommandDescription);
41-
command.AddArgument(new Argument<string>
41+
42+
var projectOption = CommonOptions.ProjectOption();
43+
var serviceOption = CommonOptions.ServiceOption();
44+
var additionalImportDirsOption = CommonOptions.AdditionalImportDirsOption();
45+
var accessOption = CommonOptions.AccessOption();
46+
var outputOption = new Option<string>(
47+
aliases: new[] { "-o", "--output" },
48+
description: CoreStrings.OutputOptionDescription);
49+
var urlArgument = new Argument<string>
4250
{
4351
Name = "url",
4452
Description = CoreStrings.AddUrlCommandArgumentDescription,
4553
Arity = ArgumentArity.ExactlyOne
46-
});
54+
};
4755

48-
var outputOption = new Option(
49-
aliases: new[] { "-o", "--output" },
50-
description: CoreStrings.OutputOptionDescription);
51-
outputOption.Argument = new Argument<string> { Name = "path", Arity = ArgumentArity.ExactlyOne };
5256
command.AddOption(outputOption);
53-
command.AddOption(CommonOptions.ProjectOption());
54-
command.AddOption(CommonOptions.ServiceOption());
55-
command.AddOption(CommonOptions.AdditionalImportDirsOption());
56-
command.AddOption(CommonOptions.AccessOption());
57-
58-
command.Handler = CommandHandler.Create<IConsole, FileInfo, Services, Access, string?, string, string>(
59-
async (console, project, services, access, additionalImportDirs, url, output) =>
57+
command.AddOption(projectOption);
58+
command.AddOption(serviceOption);
59+
command.AddOption(additionalImportDirsOption);
60+
command.AddOption(accessOption);
61+
command.AddArgument(urlArgument);
62+
63+
command.SetHandler<string, Services, Access, string?, string, string, InvocationContext, IConsole>(
64+
async (project, services, access, additionalImportDirs, url, output, context, console) =>
6065
{
6166
try
6267
{
@@ -65,18 +70,18 @@ public static Command Create()
6570
throw new CLIToolException(CoreStrings.ErrorNoOutputProvided);
6671
}
6772

68-
var command = new AddUrlCommand(console, project);
73+
var command = new AddUrlCommand(console, project, httpClient);
6974
await command.AddUrlAsync(services, access, additionalImportDirs, url, output);
7075

71-
return 0;
76+
context.ExitCode = 0;
7277
}
7378
catch (CLIToolException e)
7479
{
7580
console.LogError(e);
7681

77-
return -1;
82+
context.ExitCode = -1;
7883
}
79-
});
84+
}, projectOption, serviceOption, accessOption, additionalImportDirsOption, urlArgument, outputOption);
8085

8186
return command;
8287
}

src/dotnet-grpc/Commands/CommandBase.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ internal class CommandBase
4747

4848
private readonly HttpClient _httpClient;
4949

50-
public CommandBase(IConsole console, FileInfo? projectPath)
51-
: this(console, ResolveProject(projectPath), new HttpClient()) { }
50+
public CommandBase(IConsole console, string? projectPath, HttpClient client)
51+
: this(console, ResolveProject(projectPath), client) { }
5252

5353
// Internal for testing
5454
internal CommandBase(IConsole console, Project project)
@@ -219,20 +219,29 @@ public void AddProtobufReference(Services services, string? additionalImportDirs
219219
}
220220
}
221221

222-
public static Project ResolveProject(FileInfo? project)
222+
public static Project ResolveProject(string? project)
223223
{
224224
if (project != null)
225225
{
226-
if (!File.Exists(project.FullName))
226+
if (File.Exists(project))
227227
{
228-
throw new CLIToolException(string.Format(CultureInfo.CurrentCulture, CoreStrings.ErrorProjectDoesNotExist, project.FullName));
228+
return new Project(project);
229+
}
230+
if (Directory.Exists(project))
231+
{
232+
return LoadFromDirectoryPath(project);
229233
}
230234

231-
return new Project(project.FullName);
235+
throw new CLIToolException(string.Format(CultureInfo.CurrentCulture, CoreStrings.ErrorProjectDoesNotExist, project));
232236
}
233237

234238
var currentDirectory = Directory.GetCurrentDirectory();
235-
var projectFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj");
239+
return LoadFromDirectoryPath(currentDirectory);
240+
}
241+
242+
private static Project LoadFromDirectoryPath(string currentDirectory)
243+
{
244+
var projectFiles = Directory.GetFiles(currentDirectory, "*.csproj");
236245

237246
if (projectFiles.Length == 0)
238247
{

src/dotnet-grpc/Commands/ListCommand.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,35 @@ namespace Grpc.Dotnet.Cli.Commands
2929
{
3030
internal class ListCommand : CommandBase
3131
{
32-
public ListCommand(IConsole console, FileInfo? projectPath)
33-
: base(console, projectPath) { }
32+
public ListCommand(IConsole console, string? projectPath, HttpClient httpClient)
33+
: base(console, projectPath, httpClient) { }
3434

35-
public static Command Create()
35+
public static Command Create(HttpClient httpClient)
3636
{
3737
var command = new Command(
3838
name: "list",
3939
description: CoreStrings.ListCommandDescription);
40-
command.AddOption(CommonOptions.ProjectOption());
40+
var projectOption = CommonOptions.ProjectOption();
4141

42-
command.Handler = CommandHandler.Create<IConsole, FileInfo>(
43-
(console, project) =>
42+
command.AddOption(projectOption);
43+
44+
command.SetHandler<string, InvocationContext, IConsole>(
45+
(project, context, console) =>
4446
{
4547
try
4648
{
47-
var command = new ListCommand(console, project);
49+
var command = new ListCommand(console, project, httpClient);
4850
command.List();
4951

50-
return 0;
52+
context.ExitCode = 0;
5153
}
5254
catch (CLIToolException e)
5355
{
5456
console.LogError(e);
5557

56-
return -1;
58+
context.ExitCode = -1;
5759
}
58-
});
60+
}, projectOption);
5961

6062
return command;
6163
}

0 commit comments

Comments
 (0)