diff --git a/resources/scripts/Generator/Commands/ColorGeneratorCommand.cs b/resources/scripts/Generator/Commands/ColorGeneratorCommand.cs index 6088cdf0..bcc9aee2 100644 --- a/resources/scripts/Generator/Commands/ColorGeneratorCommand.cs +++ b/resources/scripts/Generator/Commands/ColorGeneratorCommand.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading; using Generator.Models; using Scriban; using Spectre.Console.Cli; @@ -21,7 +22,7 @@ namespace Generator.Commands public string Input { get; set; } } - public override int Execute(CommandContext context, Settings settings) + public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { var templates = new FilePath[] { diff --git a/resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs b/resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs index 1af0cbd7..b4c1ea1c 100644 --- a/resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs +++ b/resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using AngleSharp.Html.Parser; using Generator.Models; @@ -39,7 +40,7 @@ namespace Generator.Commands _parser = new HtmlParser(); } - public override async Task ExecuteAsync(CommandContext context, Settings settings) + public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) { var output = new DirectoryPath(settings.Output); if (!_fileSystem.Directory.Exists(settings.Output)) diff --git a/resources/scripts/Generator/Commands/SampleCommand.cs b/resources/scripts/Generator/Commands/SampleCommand.cs index 5c6f6ff6..27566235 100644 --- a/resources/scripts/Generator/Commands/SampleCommand.cs +++ b/resources/scripts/Generator/Commands/SampleCommand.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; +using System.Threading; using Generator.Commands.Samples; using Spectre.Console; using Spectre.Console.Cli; @@ -38,7 +39,7 @@ namespace Generator.Commands _console = new AsciiCastConsole(console); } - public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) + public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings, CancellationToken cancellationToken) { var samples = typeof(BaseSample).Assembly .GetTypes() diff --git a/resources/scripts/Generator/Commands/SpinnerGeneratorCommand.cs b/resources/scripts/Generator/Commands/SpinnerGeneratorCommand.cs index 7dd4497d..de59cfdc 100644 --- a/resources/scripts/Generator/Commands/SpinnerGeneratorCommand.cs +++ b/resources/scripts/Generator/Commands/SpinnerGeneratorCommand.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Threading; using Generator.Models; using Scriban; using Spectre.Console.Cli; @@ -16,7 +17,7 @@ namespace Generator.Commands _fileSystem = new FileSystem(); } - public override int Execute(CommandContext context, GeneratorSettings settings) + public override int Execute(CommandContext context, GeneratorSettings settings, CancellationToken cancellationToken) { // Read the spinner model. var spinners = new List(); diff --git a/src/.editorconfig b/src/.editorconfig index e01b77d5..2ab11780 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -100,5 +100,8 @@ dotnet_diagnostic.RCS1047.severity = none # RCS1090: Call 'ConfigureAwait(false)'. dotnet_diagnostic.RCS1090.severity = warning -# The file header is missing or not located at the top of the file -dotnet_diagnostic.SA1633.severity = none \ No newline at end of file +# SA1633: The file header is missing or not located at the top of the file +dotnet_diagnostic.SA1633.severity = none + +# CA2016: Forward the CancellationToken parameter to methods that take one +dotnet_diagnostic.CA2016.severity = warning \ No newline at end of file diff --git a/src/Spectre.Console.Cli/AsyncCommand.cs b/src/Spectre.Console.Cli/AsyncCommand.cs index 5dd12090..55fb3166 100644 --- a/src/Spectre.Console.Cli/AsyncCommand.cs +++ b/src/Spectre.Console.Cli/AsyncCommand.cs @@ -9,19 +9,20 @@ public abstract class AsyncCommand : ICommand /// Executes the command. /// /// The command context. + /// A that can be used to abort the command. /// An integer indicating whether or not the command executed successfully. - public abstract Task ExecuteAsync(CommandContext context); + public abstract Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken); /// - Task ICommand.Execute(CommandContext context, EmptyCommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken) { - return ExecuteAsync(context); + return ExecuteAsync(context, cancellationToken); } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken) { - return ExecuteAsync(context); + return ExecuteAsync(context, cancellationToken); } /// diff --git a/src/Spectre.Console.Cli/AsyncCommandOfT.cs b/src/Spectre.Console.Cli/AsyncCommandOfT.cs index 87da022a..5172b681 100644 --- a/src/Spectre.Console.Cli/AsyncCommandOfT.cs +++ b/src/Spectre.Console.Cli/AsyncCommandOfT.cs @@ -23,8 +23,9 @@ public abstract class AsyncCommand : ICommand /// /// The command context. /// The settings. + /// A that can be used to abort the command. /// An integer indicating whether or not the command executed successfully. - public abstract Task ExecuteAsync(CommandContext context, TSettings settings); + public abstract Task ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken); /// ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) @@ -33,15 +34,15 @@ public abstract class AsyncCommand : ICommand } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken) { Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); - return ExecuteAsync(context, (TSettings)settings); + return ExecuteAsync(context, (TSettings)settings, cancellationToken); } /// - Task ICommand.Execute(CommandContext context, TSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken) { - return ExecuteAsync(context, settings); + return ExecuteAsync(context, settings, cancellationToken); } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Command.cs b/src/Spectre.Console.Cli/Command.cs index 2261e6f9..7d9b6e5d 100644 --- a/src/Spectre.Console.Cli/Command.cs +++ b/src/Spectre.Console.Cli/Command.cs @@ -10,19 +10,20 @@ public abstract class Command : ICommand /// Executes the command. /// /// The command context. + /// A that can be used to abort the command. /// An integer indicating whether or not the command executed successfully. - public abstract int Execute(CommandContext context); + public abstract int Execute(CommandContext context, CancellationToken cancellationToken); /// - Task ICommand.Execute(CommandContext context, EmptyCommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken) { - return Task.FromResult(Execute(context)); + return Task.FromResult(Execute(context, cancellationToken)); } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken) { - return Task.FromResult(Execute(context)); + return Task.FromResult(Execute(context, cancellationToken)); } /// diff --git a/src/Spectre.Console.Cli/CommandApp.cs b/src/Spectre.Console.Cli/CommandApp.cs index fa94e915..d60d6740 100644 --- a/src/Spectre.Console.Cli/CommandApp.cs +++ b/src/Spectre.Console.Cli/CommandApp.cs @@ -26,10 +26,7 @@ public sealed class CommandApp : ICommandApp _executor = new CommandExecutor(registrar); } - /// - /// Configures the command line application. - /// - /// The configuration. + /// public void Configure(Action configuration) { if (configuration == null) @@ -51,22 +48,14 @@ public sealed class CommandApp : ICommandApp return new DefaultCommandConfigurator(GetConfigurator().SetDefaultCommand()); } - /// - /// Runs the command line application with specified arguments. - /// - /// The arguments. - /// The exit code from the executed command. - public int Run(IEnumerable args) + /// + public int Run(IEnumerable args, CancellationToken cancellationToken = default) { - return RunAsync(args).GetAwaiter().GetResult(); + return RunAsync(args, cancellationToken).GetAwaiter().GetResult(); } - /// - /// Runs the command line application with specified arguments. - /// - /// The arguments. - /// The exit code from the executed command. - public async Task RunAsync(IEnumerable args) + /// + public async Task RunAsync(IEnumerable args, CancellationToken cancellationToken = default) { try { @@ -86,7 +75,7 @@ public sealed class CommandApp : ICommandApp } return await _executor - .Execute(_configurator, args) + .ExecuteAsync(_configurator, args, cancellationToken) .ConfigureAwait(false); } catch (Exception ex) @@ -109,6 +98,11 @@ public sealed class CommandApp : ICommandApp return _configurator.Settings.ExceptionHandler(ex, null); } + if (ex is OperationCanceledException) + { + return _configurator.Settings.CancellationExitCode; + } + // Render the exception. var pretty = GetRenderableErrorMessage(ex); if (pretty != null) diff --git a/src/Spectre.Console.Cli/CommandAppOfT.cs b/src/Spectre.Console.Cli/CommandAppOfT.cs index 6df8539f..a06e43bd 100644 --- a/src/Spectre.Console.Cli/CommandAppOfT.cs +++ b/src/Spectre.Console.Cli/CommandAppOfT.cs @@ -25,33 +25,22 @@ public sealed class CommandApp : ICommandApp _defaultCommandConfigurator = _app.SetDefaultCommand(); } - /// - /// Configures the command line application. - /// - /// The configuration. + /// public void Configure(Action configuration) { _app.Configure(configuration); } - /// - /// Runs the command line application with specified arguments. - /// - /// The arguments. - /// The exit code from the executed command. - public int Run(IEnumerable args) + /// + public int Run(IEnumerable args, CancellationToken cancellationToken = default) { - return _app.Run(args); + return _app.Run(args, cancellationToken); } - /// - /// Runs the command line application with specified arguments. - /// - /// The arguments. - /// The exit code from the executed command. - public Task RunAsync(IEnumerable args) + /// + public Task RunAsync(IEnumerable args, CancellationToken cancellationToken = default) { - return _app.RunAsync(args); + return _app.RunAsync(args, cancellationToken); } internal Configurator GetConfigurator() diff --git a/src/Spectre.Console.Cli/CommandOfT.cs b/src/Spectre.Console.Cli/CommandOfT.cs index e2be58ef..d35823dc 100644 --- a/src/Spectre.Console.Cli/CommandOfT.cs +++ b/src/Spectre.Console.Cli/CommandOfT.cs @@ -24,8 +24,9 @@ public abstract class Command : ICommand /// /// The command context. /// The settings. + /// A that can be used to abort the command. /// An integer indicating whether or not the command executed successfully. - public abstract int Execute(CommandContext context, TSettings settings); + public abstract int Execute(CommandContext context, TSettings settings, CancellationToken cancellationToken); /// ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) @@ -34,15 +35,15 @@ public abstract class Command : ICommand } /// - Task ICommand.Execute(CommandContext context, CommandSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken) { Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); - return Task.FromResult(Execute(context, (TSettings)settings)); + return Task.FromResult(Execute(context, (TSettings)settings, cancellationToken)); } /// - Task ICommand.Execute(CommandContext context, TSettings settings) + Task ICommand.ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken) { - return Task.FromResult(Execute(context, settings)); + return Task.FromResult(Execute(context, settings, cancellationToken)); } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs index 6a03617c..87f6272a 100644 --- a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs +++ b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs @@ -201,6 +201,24 @@ public static class ConfiguratorExtensions return configurator; } + /// + /// Tells the command line application to return the specified exit code when it's aborted through the . + /// The default cancellation exit code is 130. + /// + /// The configurator. + /// The exit code to return in case of cancellation. + /// A configurator that can be used to configure the application further. + public static IConfigurator CancellationExitCode(this IConfigurator configurator, int exitCode) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.CancellationExitCode = exitCode; + return configurator; + } + /// /// Configures case sensitivity. /// @@ -304,14 +322,14 @@ public static class ConfiguratorExtensions public static ICommandConfigurator AddDelegate( this IConfigurator configurator, string name, - Func func) + Func func) { if (configurator == null) { throw new ArgumentNullException(nameof(configurator)); } - return configurator.AddDelegate(name, (c, _) => func(c)); + return configurator.AddDelegate(name, (c, _, ct) => func(c, ct)); } /// @@ -324,14 +342,14 @@ public static class ConfiguratorExtensions public static ICommandConfigurator AddAsyncDelegate( this IConfigurator configurator, string name, - Func> func) + Func> func) { if (configurator == null) { throw new ArgumentNullException(nameof(configurator)); } - return configurator.AddAsyncDelegate(name, (c, _) => func(c)); + return configurator.AddAsyncDelegate(name, (c, _, ct) => func(c, ct)); } /// @@ -345,7 +363,7 @@ public static class ConfiguratorExtensions public static ICommandConfigurator AddDelegate( this IConfigurator? configurator, string name, - Func func) + Func func) where TSettings : CommandSettings { if (typeof(TSettings).IsAbstract) @@ -358,7 +376,7 @@ public static class ConfiguratorExtensions throw new ArgumentNullException(nameof(configurator)); } - return configurator.AddDelegate(name, (c, _) => func(c)); + return configurator.AddDelegate(name, (c, _, ct) => func(c, ct)); } /// @@ -372,7 +390,7 @@ public static class ConfiguratorExtensions public static ICommandConfigurator AddAsyncDelegate( this IConfigurator configurator, string name, - Func> func) + Func> func) where TSettings : CommandSettings { if (configurator == null) @@ -380,7 +398,7 @@ public static class ConfiguratorExtensions throw new ArgumentNullException(nameof(configurator)); } - return configurator.AddAsyncDelegate(name, (c, _) => func(c)); + return configurator.AddAsyncDelegate(name, (c, _, ct) => func(c, ct)); } /// diff --git a/src/Spectre.Console.Cli/ICommand.cs b/src/Spectre.Console.Cli/ICommand.cs index 8cbcbf77..90ce2caf 100644 --- a/src/Spectre.Console.Cli/ICommand.cs +++ b/src/Spectre.Console.Cli/ICommand.cs @@ -18,6 +18,7 @@ public interface ICommand /// /// The command context. /// The settings. + /// A that can be used to abort the command. /// The validation result. - Task Execute(CommandContext context, CommandSettings settings); + Task ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/ICommandApp.cs b/src/Spectre.Console.Cli/ICommandApp.cs index 5d5e2e74..381a2812 100644 --- a/src/Spectre.Console.Cli/ICommandApp.cs +++ b/src/Spectre.Console.Cli/ICommandApp.cs @@ -15,13 +15,15 @@ public interface ICommandApp /// Runs the command line application with specified arguments. /// /// The arguments. + /// A that can be used to abort the application. /// The exit code from the executed command. - int Run(IEnumerable args); + int Run(IEnumerable args, CancellationToken cancellationToken = default); /// /// Runs the command line application with specified arguments. /// /// The arguments. + /// A that can be used to abort the application. /// The exit code from the executed command. - Task RunAsync(IEnumerable args); + Task RunAsync(IEnumerable args, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/ICommandAppSettings.cs b/src/Spectre.Console.Cli/ICommandAppSettings.cs index 0c865e28..d0e78517 100644 --- a/src/Spectre.Console.Cli/ICommandAppSettings.cs +++ b/src/Spectre.Console.Cli/ICommandAppSettings.cs @@ -88,6 +88,12 @@ public interface ICommandAppSettings /// bool PropagateExceptions { get; set; } + /// + /// Gets or sets the value used as the application exit code when it's aborted through the . + /// The default cancellation exit code is 130. + /// + int CancellationExitCode { get; set; } + /// /// Gets or sets a value indicating whether or not examples should be validated. /// diff --git a/src/Spectre.Console.Cli/ICommandOfT.cs b/src/Spectre.Console.Cli/ICommandOfT.cs index 5b92cf8b..bb3b03ee 100644 --- a/src/Spectre.Console.Cli/ICommandOfT.cs +++ b/src/Spectre.Console.Cli/ICommandOfT.cs @@ -12,6 +12,7 @@ public interface ICommand : ICommandLimiter /// /// The command context. /// The settings. + /// A that can be used to abort the command. /// An integer indicating whether or not the command executed successfully. - Task Execute(CommandContext context, TSettings settings); + Task ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/IConfigurator.cs b/src/Spectre.Console.Cli/IConfigurator.cs index 4e55ce34..ec4051c0 100644 --- a/src/Spectre.Console.Cli/IConfigurator.cs +++ b/src/Spectre.Console.Cli/IConfigurator.cs @@ -48,7 +48,7 @@ public interface IConfigurator /// The name of the command. /// The delegate to execute as part of command execution. /// A command configurator that can be used to configure the command further. - ICommandConfigurator AddDelegate(string name, Func func) + ICommandConfigurator AddDelegate(string name, Func func) where TSettings : CommandSettings; /// @@ -58,7 +58,7 @@ public interface IConfigurator /// The name of the command. /// The delegate to execute as part of command execution. /// A command configurator that can be used to configure the command further. - ICommandConfigurator AddAsyncDelegate(string name, Func> func) + ICommandConfigurator AddAsyncDelegate(string name, Func> func) where TSettings : CommandSettings; /// diff --git a/src/Spectre.Console.Cli/IConfiguratorOfT.cs b/src/Spectre.Console.Cli/IConfiguratorOfT.cs index 0c71b348..7c0b26f2 100644 --- a/src/Spectre.Console.Cli/IConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/IConfiguratorOfT.cs @@ -54,7 +54,7 @@ public interface IConfigurator /// The name of the command. /// The delegate to execute as part of command execution. /// A command configurator that can be used to configure the command further. - ICommandConfigurator AddDelegate(string name, Func func) + ICommandConfigurator AddDelegate(string name, Func func) where TDerivedSettings : TSettings; /// @@ -64,7 +64,7 @@ public interface IConfigurator /// The name of the command. /// The delegate to execute as part of command execution. /// A command configurator that can be used to configure the command further. - ICommandConfigurator AddAsyncDelegate(string name, Func> func) + ICommandConfigurator AddAsyncDelegate(string name, Func> func) where TDerivedSettings : TSettings; /// diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs index 5bfbc215..4d1d7dec 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -12,7 +12,7 @@ internal sealed class CommandExecutor _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); } - public async Task Execute(IConfiguration configuration, IEnumerable args) + public async Task ExecuteAsync(IConfiguration configuration, IEnumerable args, CancellationToken cancellationToken) { CommandTreeParserResult parsedResult; @@ -125,7 +125,7 @@ internal sealed class CommandExecutor leaf.Command.Data); // Execute the command tree. - return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); + return await ExecuteAsync(leaf, parsedResult.Tree, context, resolver, configuration, cancellationToken).ConfigureAwait(false); } } @@ -222,12 +222,13 @@ internal sealed class CommandExecutor return (parsedResult, tokenizerResult); } - private static async Task Execute( + private static async Task ExecuteAsync( CommandTree leaf, CommandTree tree, CommandContext context, ITypeResolver resolver, - IConfiguration configuration) + IConfiguration configuration, + CancellationToken cancellationToken) { try { @@ -256,7 +257,7 @@ internal sealed class CommandExecutor } // Execute the command. - var result = await command.Execute(context, settings); + var result = await command.ExecuteAsync(context, settings, cancellationToken); foreach (var interceptor in interceptors) { interceptor.InterceptResult(context, settings, ref result); diff --git a/src/Spectre.Console.Cli/Internal/Commands/ExplainCommand.cs b/src/Spectre.Console.Cli/Internal/Commands/ExplainCommand.cs index c1576beb..29744fca 100644 --- a/src/Spectre.Console.Cli/Internal/Commands/ExplainCommand.cs +++ b/src/Spectre.Console.Cli/Internal/Commands/ExplainCommand.cs @@ -27,7 +27,7 @@ internal sealed class ExplainCommand : Command, IBuiltI public bool IncludeHidden { get; set; } } - public override int Execute(CommandContext context, Settings settings) + public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { var tree = new Tree("CLI Configuration"); tree.AddNode(ValueMarkup("Application Name", _commandModel.ApplicationName, "no application name")); diff --git a/src/Spectre.Console.Cli/Internal/Commands/OpenCliGeneratorCommand.cs b/src/Spectre.Console.Cli/Internal/Commands/OpenCliGeneratorCommand.cs index 7d3e444a..e33b058a 100644 --- a/src/Spectre.Console.Cli/Internal/Commands/OpenCliGeneratorCommand.cs +++ b/src/Spectre.Console.Cli/Internal/Commands/OpenCliGeneratorCommand.cs @@ -13,7 +13,7 @@ internal sealed class OpenCliGeneratorCommand : Command, IBuiltInCommand _model = model ?? throw new ArgumentNullException(nameof(model)); } - public override int Execute(CommandContext context) + public override int Execute(CommandContext context, CancellationToken cancellationToken) { var document = new OpenCliDocument { diff --git a/src/Spectre.Console.Cli/Internal/Commands/VersionCommand.cs b/src/Spectre.Console.Cli/Internal/Commands/VersionCommand.cs index 419171e7..c6bef190 100644 --- a/src/Spectre.Console.Cli/Internal/Commands/VersionCommand.cs +++ b/src/Spectre.Console.Cli/Internal/Commands/VersionCommand.cs @@ -11,7 +11,7 @@ internal sealed class VersionCommand : Command, IBuiltInCommand _writer = configuration.Settings.Console.GetConsole(); } - public override int Execute(CommandContext context) + public override int Execute(CommandContext context, CancellationToken cancellationToken) { _writer.MarkupLine( "[yellow]Spectre.Cli[/] version [aqua]{0}[/]", diff --git a/src/Spectre.Console.Cli/Internal/Commands/XmlDocCommand.cs b/src/Spectre.Console.Cli/Internal/Commands/XmlDocCommand.cs index 2b322e02..8b2e82ed 100644 --- a/src/Spectre.Console.Cli/Internal/Commands/XmlDocCommand.cs +++ b/src/Spectre.Console.Cli/Internal/Commands/XmlDocCommand.cs @@ -13,7 +13,7 @@ internal sealed class XmlDocCommand : Command, IBuiltInCommand _writer = configuration.Settings.Console.GetConsole(); } - public override int Execute(CommandContext context) + public override int Execute(CommandContext context, CancellationToken cancellationToken) { _writer.Write(Serialize(_model), Style.Plain); return 0; diff --git a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs index 91b51a49..65b66907 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs @@ -18,6 +18,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings public HelpProviderStyle? HelpProviderStyles { get; set; } public bool StrictParsing { get; set; } public bool ConvertFlagsToRemainingArguments { get; set; } + public int CancellationExitCode { get; set; } public ParsingMode ParsingMode => StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; @@ -33,6 +34,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings TrimTrailingPeriod = true; HelpProviderStyles = HelpProviderStyle.Default; ConvertFlagsToRemainingArguments = false; + CancellationExitCode = 130; } public bool IsTrue(Func func, string environmentVariableName) diff --git a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs index 5f2d20a2..4605e011 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs @@ -56,19 +56,19 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig return new CommandConfigurator(command); } - public ICommandConfigurator AddDelegate(string name, Func func) + public ICommandConfigurator AddDelegate(string name, Func func) where TSettings : CommandSettings { var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate( - name, (context, settings) => Task.FromResult(func(context, (TSettings)settings)))); + name, (context, settings, cancellationToken) => Task.FromResult(func(context, (TSettings)settings, cancellationToken)))); return new CommandConfigurator(command); } - public ICommandConfigurator AddAsyncDelegate(string name, Func> func) + public ICommandConfigurator AddAsyncDelegate(string name, Func> func) where TSettings : CommandSettings { var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate( - name, (context, settings) => func(context, (TSettings)settings))); + name, (context, settings, cancellationToken) => func(context, (TSettings)settings, cancellationToken))); return new CommandConfigurator(command); } diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs index 537e4854..05c8ef95 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs @@ -46,21 +46,21 @@ internal sealed class Configurator : IUnsafeBranchConfigurator, IConf return configurator; } - public ICommandConfigurator AddDelegate(string name, Func func) + public ICommandConfigurator AddDelegate(string name, Func func) where TDerivedSettings : TSettings { var command = ConfiguredCommand.FromDelegate( - name, (context, settings) => Task.FromResult(func(context, (TDerivedSettings)settings))); + name, (context, settings, cancellationToken) => Task.FromResult(func(context, (TDerivedSettings)settings, cancellationToken))); _command.Children.Add(command); return new CommandConfigurator(command); } - public ICommandConfigurator AddAsyncDelegate(string name, Func> func) + public ICommandConfigurator AddAsyncDelegate(string name, Func> func) where TDerivedSettings : TSettings { var command = ConfiguredCommand.FromDelegate( - name, (context, settings) => func(context, (TDerivedSettings)settings)); + name, (context, settings, cancellationToken) => func(context, (TDerivedSettings)settings, cancellationToken)); _command.Children.Add(command); return new CommandConfigurator(command); diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs index e39663f1..75ad9ddb 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs @@ -8,7 +8,7 @@ internal sealed class ConfiguredCommand public object? Data { get; set; } public Type? CommandType { get; } public Type SettingsType { get; } - public Func>? Delegate { get; } + public Func>? Delegate { get; } public bool IsDefaultCommand { get; } public bool IsHidden { get; set; } @@ -19,7 +19,7 @@ internal sealed class ConfiguredCommand string name, Type? commandType, Type settingsType, - Func>? @delegate, + Func>? @delegate, bool isDefaultCommand) { Name = name; @@ -60,7 +60,7 @@ internal sealed class ConfiguredCommand } public static ConfiguredCommand FromDelegate( - string name, Func>? @delegate = null) + string name, Func>? @delegate = null) where TSettings : CommandSettings { return new ConfiguredCommand(name, null, typeof(TSettings), @delegate, false); diff --git a/src/Spectre.Console.Cli/Internal/DelegateCommand.cs b/src/Spectre.Console.Cli/Internal/DelegateCommand.cs index 4786fdd4..30825bcb 100644 --- a/src/Spectre.Console.Cli/Internal/DelegateCommand.cs +++ b/src/Spectre.Console.Cli/Internal/DelegateCommand.cs @@ -2,16 +2,16 @@ namespace Spectre.Console.Cli; internal sealed class DelegateCommand : ICommand { - private readonly Func> _func; + private readonly Func> _func; - public DelegateCommand(Func> func) + public DelegateCommand(Func> func) { _func = func; } - public Task Execute(CommandContext context, CommandSettings settings) + public Task ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken) { - return _func(context, settings); + return _func(context, settings, cancellationToken); } public ValidationResult Validate(CommandContext context, CommandSettings settings) diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs index 08bad48d..e96ed339 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs @@ -8,7 +8,7 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo public object? Data { get; } public Type? CommandType { get; } public Type SettingsType { get; } - public Func>? Delegate { get; } + public Func>? Delegate { get; } public bool IsDefaultCommand { get; } public CommandInfo? Parent { get; } public IList Children { get; } diff --git a/src/Spectre.Console.Testing/Cli/CommandAppTester.cs b/src/Spectre.Console.Testing/Cli/CommandAppTester.cs index 3817903e..f2156e95 100644 --- a/src/Spectre.Console.Testing/Cli/CommandAppTester.cs +++ b/src/Spectre.Console.Testing/Cli/CommandAppTester.cs @@ -98,7 +98,7 @@ public sealed class CommandAppTester { try { - Run(args, Console, c => c.PropagateExceptions()); + RunAsync(args, Console, c => c.PropagateExceptions()).GetAwaiter().GetResult(); throw new InvalidOperationException("Expected an exception to be thrown, but there was none."); } catch (T ex) @@ -129,53 +129,21 @@ public sealed class CommandAppTester /// The result. public CommandAppResult Run(params string[] args) { - return Run(args, Console); - } - - private CommandAppResult Run(string[] args, TestConsole console, Action? config = null) - { - CommandContext? context = null; - CommandSettings? settings = null; - - var app = new CommandApp(Registrar); - _appConfiguration?.Invoke(app); - - if (_configuration != null) - { - app.Configure(_configuration); - } - - if (config != null) - { - app.Configure(config); - } - - app.Configure(c => c.ConfigureConsole(console)); - app.Configure(c => c.SetInterceptor(new CallbackCommandInterceptor((ctx, s) => - { - context = ctx; - settings = s; - }))); - - var result = app.Run(args); - - var output = console.Output.NormalizeLineEndings(); - output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output; - - return new CommandAppResult(result, output, context, settings); + return RunAsync(args, Console).GetAwaiter().GetResult(); } /// /// Runs the command application asynchronously. /// /// The arguments. + /// The token to monitor for cancellation requests. /// The result. - public async Task RunAsync(params string[] args) + public async Task RunAsync(string[]? args = null, CancellationToken cancellationToken = default) { - return await RunAsync(args, Console); + return await RunAsync(args ?? [], Console, cancellationToken: cancellationToken); } - private async Task RunAsync(string[] args, TestConsole console, Action? config = null) + private async Task RunAsync(string[] args, TestConsole console, Action? config = null, CancellationToken cancellationToken = default) { CommandContext? context = null; CommandSettings? settings = null; @@ -200,7 +168,7 @@ public sealed class CommandAppTester settings = s; }))); - var result = await app.RunAsync(args); + var result = await app.RunAsync(args, cancellationToken); var output = console.Output.NormalizeLineEndings(); output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output; diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/AsynchronousCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/AsynchronousCommand.cs index 0da13ac2..891d1ce9 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/AsynchronousCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/AsynchronousCommand.cs @@ -9,10 +9,10 @@ public sealed class AsynchronousCommand : AsyncCommand ExecuteAsync(CommandContext context, AsynchronousCommandSettings settings) + public async override Task ExecuteAsync(CommandContext context, AsynchronousCommandSettings settings, CancellationToken cancellationToken) { // Simulate a long running asynchronous task - await Task.Delay(200); + await Task.Delay(200, cancellationToken); if (settings.ThrowException) { diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/CatCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/CatCommand.cs index ab0a6afa..97eb2b43 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/CatCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/CatCommand.cs @@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data; public class CatCommand : AnimalCommand { - public override int Execute(CommandContext context, CatSettings settings) + public override int Execute(CommandContext context, CatSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DogCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DogCommand.cs index 1bb5e3ea..9361fdae 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DogCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DogCommand.cs @@ -23,7 +23,7 @@ public class DogCommand : AnimalCommand return base.Validate(context, settings); } - public override int Execute(CommandContext context, DogSettings settings) + public override int Execute(CommandContext context, DogSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DumpRemainingCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DumpRemainingCommand.cs index dc8e71da..b848787d 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DumpRemainingCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/DumpRemainingCommand.cs @@ -9,7 +9,7 @@ public sealed class DumpRemainingCommand : Command _console = console; } - public override int Execute(CommandContext context, EmptyCommandSettings settings) + public override int Execute(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken) { if (context.Remaining.Raw.Count > 0) { diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/EmptyCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/EmptyCommand.cs index 6ab33907..498884e2 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/EmptyCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/EmptyCommand.cs @@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data; public sealed class EmptyCommand : Command { - public override int Execute(CommandContext context, EmptyCommandSettings settings) + public override int Execute(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GenericCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GenericCommand.cs index a0ae21cd..0353508a 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GenericCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GenericCommand.cs @@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data; public sealed class GenericCommand : Command where TSettings : CommandSettings { - public override int Execute(CommandContext context, TSettings settings) + public override int Execute(CommandContext context, TSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GiraffeCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GiraffeCommand.cs index 87d0f04d..e6569ae3 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GiraffeCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GiraffeCommand.cs @@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data; [Description("The giraffe command.")] public sealed class GiraffeCommand : Command { - public override int Execute(CommandContext context, GiraffeSettings settings) + public override int Execute(CommandContext context, GiraffeSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GreeterCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GreeterCommand.cs index d466f460..03ff8eae 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GreeterCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/GreeterCommand.cs @@ -9,7 +9,7 @@ public class GreeterCommand : Command _console = console; } - public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings) + public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings, CancellationToken cancellationToken) { _console.WriteLine(settings.Greeting); return 0; diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HiddenOptionsCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HiddenOptionsCommand.cs index dded24cf..15dcfb60 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HiddenOptionsCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HiddenOptionsCommand.cs @@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data; public sealed class HiddenOptionsCommand : Command { - public override int Execute(CommandContext context, HiddenOptionSettings settings) + public override int Execute(CommandContext context, HiddenOptionSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HorseCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HorseCommand.cs index 148999cd..7e7e89e6 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HorseCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/HorseCommand.cs @@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data; [Description("The horse command.")] public class HorseCommand : AnimalCommand { - public override int Execute(CommandContext context, HorseSettings settings) + public override int Execute(CommandContext context, HorseSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/InvalidCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/InvalidCommand.cs index 27ee2054..ef2cf1a5 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/InvalidCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/InvalidCommand.cs @@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data; public sealed class InvalidCommand : Command { - public override int Execute(CommandContext context, InvalidSettings settings) + public override int Execute(CommandContext context, InvalidSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/LionCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/LionCommand.cs index eb7dfa54..bd415d69 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/LionCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/LionCommand.cs @@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data; [Description("The lion command.")] public class LionCommand : AnimalCommand { - public override int Execute(CommandContext context, LionSettings settings) + public override int Execute(CommandContext context, LionSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/NoDescriptionCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/NoDescriptionCommand.cs index 5e9c0bc9..1a7d064b 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/NoDescriptionCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/NoDescriptionCommand.cs @@ -5,7 +5,7 @@ public sealed class NoDescriptionCommand : Command [CommandOption("-f|--foo ")] public int Foo { get; set; } - public override int Execute(CommandContext context, EmptyCommandSettings settings) + public override int Execute(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/OptionVectorCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/OptionVectorCommand.cs index a892881e..bfb76e78 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/OptionVectorCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/OptionVectorCommand.cs @@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data; public class OptionVectorCommand : Command { - public override int Execute(CommandContext context, OptionVectorSettings settings) + public override int Execute(CommandContext context, OptionVectorSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/ThrowingCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/ThrowingCommand.cs index 48045250..493c3f14 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/ThrowingCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/ThrowingCommand.cs @@ -2,7 +2,7 @@ namespace Spectre.Console.Tests.Data; public sealed class ThrowingCommand : Command { - public override int Execute(CommandContext context, ThrowingCommandSettings settings) + public override int Execute(CommandContext context, ThrowingCommandSettings settings, CancellationToken cancellationToken) { throw new InvalidOperationException("W00t?"); } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/TurtleCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/TurtleCommand.cs index 0b99327b..957671a5 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/TurtleCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/TurtleCommand.cs @@ -3,7 +3,7 @@ namespace Spectre.Console.Tests.Data; [Description("The turtle command.")] public class TurtleCommand : AnimalCommand { - public override int Execute(CommandContext context, TurtleSettings settings) + public override int Execute(CommandContext context, TurtleSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/VersionCommand.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/VersionCommand.cs index 55188370..8c7c3303 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/VersionCommand.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Commands/VersionCommand.cs @@ -9,7 +9,7 @@ public sealed class VersionCommand : Command _console = console; } - public override int Execute(CommandContext context, VersionSettings settings) + public override int Execute(CommandContext context, VersionSettings settings, CancellationToken cancellationToken) { _console.WriteLine($"VersionCommand ran, Version: {settings.Version ?? string.Empty}"); diff --git a/src/Tests/Spectre.Console.Cli.Tests/Properties/Usings.cs b/src/Tests/Spectre.Console.Cli.Tests/Properties/Usings.cs index 7436882c..6b28670a 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Properties/Usings.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Properties/Usings.cs @@ -5,6 +5,7 @@ global using System.Diagnostics.CodeAnalysis; global using System.Globalization; global using System.Linq; global using System.Runtime.CompilerServices; +global using System.Threading; global using System.Threading.Tasks; global using Shouldly; global using Spectre.Console.Cli; diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Async.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Async.cs index 8ce3ee14..892a3b31 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Async.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Async.cs @@ -53,7 +53,7 @@ public sealed partial class CommandAppTests }); // When - var result = await Record.ExceptionAsync(async () => + var exception = await Record.ExceptionAsync(async () => await app.RunAsync(new[] { "--ThrowException", @@ -61,10 +61,64 @@ public sealed partial class CommandAppTests })); // Then - result.ShouldBeOfType().And(ex => + exception.ShouldBeOfType().And(ex => { ex.Message.ShouldBe("Throwing exception asynchronously"); }); } + + [Fact] + public async Task Should_Throw_OperationCanceledException_When_Propagated_And_Cancelled() + { + // Given + var app = new CommandAppTester(); + app.SetDefaultCommand(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var exception = await Record.ExceptionAsync(async () => + await app.RunAsync(cancellationToken: new CancellationToken(canceled: true))); + + // Then + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); + } + + [Fact] + public async Task Should_Return_Default_Exit_Code_When_Cancelled() + { + // Given + var app = new CommandAppTester(); + app.SetDefaultCommand(); + + // When + var result = await app.RunAsync(cancellationToken: new CancellationToken(canceled: true)); + + // Then + result.ExitCode.ShouldBe(130); + result.Output.ShouldBeEmpty(); + } + + [Fact] + public async Task Should_Return_Custom_Exit_Code_When_Cancelled() + { + // Given + var app = new CommandAppTester(); + app.SetDefaultCommand(); + app.Configure(config => + { + config.CancellationExitCode(123); + }); + + // When + var result = await app.RunAsync(cancellationToken: new CancellationToken(canceled: true)); + + // Then + result.ExitCode.ShouldBe(123); + result.Output.ShouldBeEmpty(); + } } } \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Constructor.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Constructor.cs index 96d329aa..2385a01c 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Constructor.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Constructor.cs @@ -28,12 +28,12 @@ public sealed partial class CommandAppTests public class NullableCommand : Command { - public override int Execute(CommandContext context, NullableSettings settings) => 0; + public override int Execute(CommandContext context, NullableSettings settings, CancellationToken cancellationToken) => 0; } public class NullableWithInitCommand : Command { - public override int Execute(CommandContext context, NullableWithInitSettings settings) => 0; + public override int Execute(CommandContext context, NullableWithInitSettings settings, CancellationToken cancellationToken) => 0; } [Fact] diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Injection.Settings.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Injection.Settings.cs index 8541ed91..b151cce2 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Injection.Settings.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Injection.Settings.cs @@ -19,7 +19,7 @@ public sealed partial class CommandAppTests _dep = dep; } - public override int Execute(CommandContext context, CustomInheritedCommandSettings settings) + public override int Execute(CommandContext context, CustomInheritedCommandSettings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs index 338e2e29..13ad0b9c 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs @@ -10,7 +10,7 @@ public sealed partial class CommandAppTests { } - public override int Execute(CommandContext context, Settings settings) + public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { return 0; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs index 34d85200..0f024025 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs @@ -1117,7 +1117,7 @@ public sealed partial class CommandAppTests { config.PropagateExceptions(); config.AddDelegate( - "foo", (context, settings) => + "foo", (context, settings, _) => { dog = settings; data = (int)context.Data; @@ -1145,7 +1145,7 @@ public sealed partial class CommandAppTests { cfg.AddBranch("a", d => { - d.AddDelegate("b", _ => 0); + d.AddDelegate("b", (_, _) => 0); }); }); @@ -1165,7 +1165,7 @@ public sealed partial class CommandAppTests var app = new CommandAppTester(); app.Configure(cfg => { - cfg.AddDelegate("a", _ => 0); + cfg.AddDelegate("a", (_, _) => 0); }); // When @@ -1189,7 +1189,7 @@ public sealed partial class CommandAppTests { config.PropagateExceptions(); config.AddAsyncDelegate( - "foo", (context, settings) => + "foo", (context, settings, _) => { dog = settings; data = (int)context.Data; @@ -1222,7 +1222,7 @@ public sealed partial class CommandAppTests config.AddBranch("foo", foo => { foo.AddDelegate( - "bar", (context, settings) => + "bar", (context, settings, _) => { dog = settings; data = (int)context.Data; @@ -1256,7 +1256,7 @@ public sealed partial class CommandAppTests config.AddBranch("foo", foo => { foo.AddAsyncDelegate( - "bar", (context, settings) => + "bar", (context, settings, _) => { dog = settings; data = (int)context.Data; diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs index 7af0d4c9..28405402 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs @@ -14,7 +14,7 @@ public sealed class CommandAppTesterTests _console = console; } - public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings) + public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings, CancellationToken cancellationToken) { _console.Write(settings.Greeting); return 0; diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs index 98879e19..20a4a6ae 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs @@ -11,7 +11,7 @@ public sealed class InteractiveCommandTests _console = console; } - public override int Execute(CommandContext context) + public override int Execute(CommandContext context, CancellationToken cancellationToken) { var fruits = _console.Prompt( new MultiSelectionPrompt()