diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index 1a53e0b4d..82394b787 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -86,7 +86,7 @@ namespace GitHub.Runner.Common public static class CommandLine { //if you are adding a new arg, please make sure you update the - //validArgs array as well present in the CommandSettings.cs + //validOptions dictionary as well present in the CommandSettings.cs public static class Args { public static readonly string Auth = "auth"; @@ -121,7 +121,7 @@ namespace GitHub.Runner.Common } //if you are adding a new flag, please make sure you update the - //validFlags array as well present in the CommandSettings.cs + //validOptions dictionary as well present in the CommandSettings.cs public static class Flags { public static readonly string Check = "check"; diff --git a/src/Runner.Listener/CommandSettings.cs b/src/Runner.Listener/CommandSettings.cs index de268d859..5067cf921 100644 --- a/src/Runner.Listener/CommandSettings.cs +++ b/src/Runner.Listener/CommandSettings.cs @@ -17,43 +17,57 @@ namespace GitHub.Runner.Listener private readonly IPromptManager _promptManager; private readonly Tracing _trace; - private readonly string[] validCommands = + // Valid flags for all commands + private readonly string[] genericOptions = { - Constants.Runner.CommandLine.Commands.Configure, - Constants.Runner.CommandLine.Commands.Remove, - Constants.Runner.CommandLine.Commands.Run, - Constants.Runner.CommandLine.Commands.Warmup, - }; - - private readonly string[] validFlags = - { - Constants.Runner.CommandLine.Flags.Check, - Constants.Runner.CommandLine.Flags.Commit, - Constants.Runner.CommandLine.Flags.DisableUpdate, - Constants.Runner.CommandLine.Flags.Ephemeral, Constants.Runner.CommandLine.Flags.Help, - Constants.Runner.CommandLine.Flags.Once, - Constants.Runner.CommandLine.Flags.Replace, - Constants.Runner.CommandLine.Flags.RunAsService, - Constants.Runner.CommandLine.Flags.Unattended, - Constants.Runner.CommandLine.Flags.Version + Constants.Runner.CommandLine.Flags.Version, + Constants.Runner.CommandLine.Flags.Commit, + Constants.Runner.CommandLine.Flags.Check }; - private readonly string[] validArgs = + // Valid flags and args for specific command - key: command, value: array of valid flags and args + private readonly Dictionary validOptions = new Dictionary { - Constants.Runner.CommandLine.Args.Auth, - Constants.Runner.CommandLine.Args.Labels, - Constants.Runner.CommandLine.Args.MonitorSocketAddress, - Constants.Runner.CommandLine.Args.Name, - Constants.Runner.CommandLine.Args.PAT, - Constants.Runner.CommandLine.Args.RunnerGroup, - Constants.Runner.CommandLine.Args.StartupType, - Constants.Runner.CommandLine.Args.Token, - Constants.Runner.CommandLine.Args.Url, - Constants.Runner.CommandLine.Args.UserName, - Constants.Runner.CommandLine.Args.WindowsLogonAccount, - Constants.Runner.CommandLine.Args.WindowsLogonPassword, - Constants.Runner.CommandLine.Args.Work + // Valid configure flags and args + [Constants.Runner.CommandLine.Commands.Configure] = + new string[] + { + Constants.Runner.CommandLine.Flags.DisableUpdate, + Constants.Runner.CommandLine.Flags.Ephemeral, + Constants.Runner.CommandLine.Flags.Replace, + Constants.Runner.CommandLine.Flags.RunAsService, + Constants.Runner.CommandLine.Flags.Unattended, + Constants.Runner.CommandLine.Args.Auth, + Constants.Runner.CommandLine.Args.Labels, + Constants.Runner.CommandLine.Args.MonitorSocketAddress, + Constants.Runner.CommandLine.Args.Name, + Constants.Runner.CommandLine.Args.PAT, + Constants.Runner.CommandLine.Args.RunnerGroup, + Constants.Runner.CommandLine.Args.Token, + Constants.Runner.CommandLine.Args.Url, + Constants.Runner.CommandLine.Args.UserName, + Constants.Runner.CommandLine.Args.WindowsLogonAccount, + Constants.Runner.CommandLine.Args.WindowsLogonPassword, + Constants.Runner.CommandLine.Args.Work + }, + // Valid remove flags and args + [Constants.Runner.CommandLine.Commands.Remove] = + new string[] + { + Constants.Runner.CommandLine.Args.Token, + Constants.Runner.CommandLine.Args.PAT + }, + // Valid run flags and args + [Constants.Runner.CommandLine.Commands.Run] = + new string[] + { + Constants.Runner.CommandLine.Flags.Once, + Constants.Runner.CommandLine.Args.StartupType + }, + // valid warmup flags and args + [Constants.Runner.CommandLine.Commands.Warmup] = + new string[] { } }; // Commands. @@ -126,17 +140,48 @@ namespace GitHub.Runner.Listener List unknowns = new List(); // detect unknown commands - unknowns.AddRange(_parser.Commands.Where(x => !validCommands.Contains(x, StringComparer.OrdinalIgnoreCase))); + unknowns.AddRange(_parser.Commands.Where(x => !validOptions.Keys.Contains(x, StringComparer.OrdinalIgnoreCase))); - // detect unknown flags - unknowns.AddRange(_parser.Flags.Where(x => !validFlags.Contains(x, StringComparer.OrdinalIgnoreCase))); - - // detect unknown args - unknowns.AddRange(_parser.Args.Keys.Where(x => !validArgs.Contains(x, StringComparer.OrdinalIgnoreCase))); + if (unknowns.Count == 0) + { + // detect unknown flags and args for valid commands + foreach (var command in _parser.Commands) + { + if (validOptions.TryGetValue(command, out string[] options)) + { + unknowns.AddRange(_parser.Flags.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase) && !genericOptions.Contains(x, StringComparer.OrdinalIgnoreCase))); + unknowns.AddRange(_parser.Args.Keys.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase))); + } + } + } return unknowns; } + public string GetCommandName() + { + string command = string.Empty; + + if (Configure) + { + command = Constants.Runner.CommandLine.Commands.Configure; + } + else if (Remove) + { + command = Constants.Runner.CommandLine.Commands.Remove; + } + else if (Run) + { + command = Constants.Runner.CommandLine.Commands.Run; + } + else if (Warmup) + { + command = Constants.Runner.CommandLine.Commands.Warmup; + } + + return command; + } + // // Interactive flags. // diff --git a/src/Runner.Listener/Program.cs b/src/Runner.Listener/Program.cs index a24224dad..537cda1c7 100644 --- a/src/Runner.Listener/Program.cs +++ b/src/Runner.Listener/Program.cs @@ -95,7 +95,15 @@ namespace GitHub.Runner.Listener var unknownCommandlines = command.Validate(); if (unknownCommandlines.Count > 0) { - terminal.WriteError($"Unrecognized command-line input arguments: '{string.Join(", ", unknownCommandlines)}'. For usage refer to: .\\config.cmd --help or ./config.sh --help"); + string commandName = command.GetCommandName(); + if (string.IsNullOrEmpty(commandName)) + { + terminal.WriteError($"This command does not recognize the command-line input arguments: '{string.Join(", ", unknownCommandlines)}'. For usage refer to: .\\config.cmd --help or ./config.sh --help"); + } + else + { + terminal.WriteError($"Unrecognized command-line input arguments for command {commandName}: '{string.Join(", ", unknownCommandlines)}'. For usage refer to: .\\config.cmd --help or ./config.sh --help"); + } } // Defer to the Runner class to execute the command. diff --git a/src/Test/L0/Listener/CommandSettingsL0.cs b/src/Test/L0/Listener/CommandSettingsL0.cs index 9354e5508..014ed5a72 100644 --- a/src/Test/L0/Listener/CommandSettingsL0.cs +++ b/src/Test/L0/Listener/CommandSettingsL0.cs @@ -708,33 +708,85 @@ namespace GitHub.Runner.Common.Tests } } - [Fact] + [Theory] + [InlineData("configure", "once")] + [InlineData("remove", "disableupdate")] + [InlineData("remove", "ephemeral")] + [InlineData("remove", "once")] + [InlineData("remove", "replace")] + [InlineData("remove", "runasservice")] + [InlineData("remove", "unattended")] + [InlineData("run", "disableupdate")] + [InlineData("run", "ephemeral")] + [InlineData("run", "replace")] + [InlineData("run", "runasservice")] + [InlineData("run", "unattended")] + [InlineData("warmup", "disableupdate")] + [InlineData("warmup", "ephemeral")] + [InlineData("warmup", "once")] + [InlineData("warmup", "replace")] + [InlineData("warmup", "runasservice")] + [InlineData("warmup", "unattended")] [Trait("Level", "L0")] [Trait("Category", nameof(CommandSettings))] - public void ValidateFlags() + public void ValidateInvalidFlagCommandCombination(string validCommand, string flag) { using (TestHostContext hc = CreateTestContext()) { // Arrange. - var command = new CommandSettings(hc, args: new string[] { "--badflag" }); + var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{flag}" }); // Assert. - Assert.Contains("badflag", command.Validate()); + Assert.Contains(flag, command.Validate()); } } - [Fact] + [Theory] + [InlineData("remove", "auth", "bar arg value")] + [InlineData("remove", "labels", "bar arg value")] + [InlineData("remove", "monitorsocketaddress", "bar arg value")] + [InlineData("remove", "name", "bar arg value")] + [InlineData("remove", "runnergroup", "bar arg value")] + [InlineData("remove", "url", "bar arg value")] + [InlineData("remove", "username", "bar arg value")] + [InlineData("remove", "windowslogonaccount", "bar arg value")] + [InlineData("remove", "windowslogonpassword", "bar arg value")] + [InlineData("remove", "work", "bar arg value")] + [InlineData("run", "auth", "bad arg value")] + [InlineData("run", "labels", "bad arg value")] + [InlineData("run", "monitorsocketaddress", "bad arg value")] + [InlineData("run", "name", "bad arg value")] + [InlineData("run", "pat", "bad arg value")] + [InlineData("run", "runnergroup", "bad arg value")] + [InlineData("run", "token", "bad arg value")] + [InlineData("run", "url", "bad arg value")] + [InlineData("run", "username", "bad arg value")] + [InlineData("run", "windowslogonaccount", "bad arg value")] + [InlineData("run", "windowslogonpassword", "bad arg value")] + [InlineData("run", "work", "bad arg value")] + [InlineData("warmup", "auth", "bad arg value")] + [InlineData("warmup", "labels", "bad arg value")] + [InlineData("warmup", "monitorsocketaddress", "bad arg value")] + [InlineData("warmup", "name", "bad arg value")] + [InlineData("warmup", "pat", "bad arg value")] + [InlineData("warmup", "runnergroup", "bad arg value")] + [InlineData("warmup", "token", "bad arg value")] + [InlineData("warmup", "url", "bad arg value")] + [InlineData("warmup", "username", "bad arg value")] + [InlineData("warmup", "windowslogonaccount", "bad arg value")] + [InlineData("warmup", "windowslogonpassword", "bad arg value")] + [InlineData("warmup", "work", "bad arg value")] [Trait("Level", "L0")] [Trait("Category", nameof(CommandSettings))] - public void ValidateArgs() + public void ValidateInvalidArgCommandCombination(string validCommand, string arg, string argValue) { using (TestHostContext hc = CreateTestContext()) { // Arrange. - var command = new CommandSettings(hc, args: new string[] { "--badargname", "bad arg value" }); + var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{arg}", argValue }); // Assert. - Assert.Contains("badargname", command.Validate()); + Assert.Contains(arg, command.Validate()); } } @@ -758,6 +810,73 @@ namespace GitHub.Runner.Common.Tests } } + [Theory] + [InlineData("configure", "help")] + [InlineData("configure", "version")] + [InlineData("configure", "commit")] + [InlineData("configure", "check")] + [InlineData("configure", "disableupdate")] + [InlineData("configure", "ephemeral")] + [InlineData("configure", "replace")] + [InlineData("configure", "runasservice")] + [InlineData("configure", "unattended")] + [InlineData("remove", "help")] + [InlineData("remove", "version")] + [InlineData("remove", "commit")] + [InlineData("remove", "check")] + [InlineData("run", "help")] + [InlineData("run", "version")] + [InlineData("run", "commit")] + [InlineData("run", "check")] + [InlineData("run", "once")] + [InlineData("warmup", "help")] + [InlineData("warmup", "version")] + [InlineData("warmup", "commit")] + [InlineData("warmup", "check")] + [Trait("Level", "L0")] + [Trait("Category", nameof(CommandSettings))] + public void ValidateGoodFlagCommandCombination(string validCommand, string flag) + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{flag}" }); + + // Assert. + Assert.True(command.Validate().Count == 0); + } + } + + [Theory] + [InlineData("configure", "auth", "good arg value")] + [InlineData("configure", "labels", "good arg value")] + [InlineData("configure", "monitorsocketaddress", "good arg value")] + [InlineData("configure", "name", "good arg value")] + [InlineData("configure", "pat", "good arg value")] + [InlineData("configure", "runnergroup", "good arg value")] + [InlineData("configure", "token", "good arg value")] + [InlineData("configure", "url", "good arg value")] + [InlineData("configure", "username", "good arg value")] + [InlineData("configure", "windowslogonaccount", "good arg value")] + [InlineData("configure", "windowslogonpassword", "good arg value")] + [InlineData("configure", "work", "good arg value")] + [InlineData("remove", "token", "good arg value")] + [InlineData("remove", "pat", "good arg value")] + [InlineData("run", "startuptype", "good arg value")] + [Trait("Level", "L0")] + [Trait("Category", nameof(CommandSettings))] + public void ValidateGoodArgCommandCombination(string validCommand, string arg, string argValue) + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{arg}", argValue }); + + // Assert. + Assert.True(command.Validate().Count == 0); + } + } + private TestHostContext CreateTestContext([CallerMemberName] string testName = "") { TestHostContext hc = new TestHostContext(this, testName);