Skip to content

Adding in cancellation support for InvokeAsync #1502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ System.CommandLine
public static class CommandExtensions
public static System.Int32 Invoke(this Command command, System.String[] args, IConsole console = null)
public static System.Int32 Invoke(this Command command, System.String commandLine, IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String[] args, IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String commandLine, IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String[] args, IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Command command, System.String commandLine, IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
public static ParseResult Parse(this Command command, System.String[] args)
public static ParseResult Parse(this Command command, System.String commandLine)
public class CommandLineBuilder
Expand Down Expand Up @@ -357,8 +357,8 @@ System.CommandLine.Help
System.CommandLine.Invocation
public interface IInvocationResult
public System.Void Apply(InvocationContext context)
public class InvocationContext
.ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null)
public class InvocationContext, System.IDisposable
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonsequitur not sure if this is important to you or not, but explicit interface members are not shown.

.ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
public System.CommandLine.Binding.BindingContext BindingContext { get; }
public System.CommandLine.IConsole Console { get; set; }
public System.Int32 ExitCode { get; set; }
Expand All @@ -368,6 +368,7 @@ System.CommandLine.Invocation
public System.CommandLine.Parsing.Parser Parser { get; }
public System.CommandLine.ParseResult ParseResult { get; set; }
public System.Threading.CancellationToken GetCancellationToken()
public System.Void LinkToken(System.Threading.CancellationToken token)
public delegate InvocationMiddleware : System.MulticastDelegate, System.ICloneable, System.Runtime.Serialization.ISerializable
.ctor(System.Object object, System.IntPtr method)
public System.IAsyncResult BeginInvoke(InvocationContext context, System.Func<InvocationContext,System.Threading.Tasks.Task> next, System.AsyncCallback callback, System.Object object)
Expand Down Expand Up @@ -450,12 +451,12 @@ System.CommandLine.Parsing
public static System.String Diagram(this System.CommandLine.ParseResult parseResult)
public static System.Boolean HasOption(this System.CommandLine.ParseResult parseResult, System.CommandLine.Option option)
public static System.Int32 Invoke(this System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
public static class ParserExtensions
public static System.Int32 Invoke(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null)
public static System.Int32 Invoke(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String commandLine, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
public static System.Threading.Tasks.Task<System.Int32> InvokeAsync(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
public static System.CommandLine.ParseResult Parse(this Parser parser, System.String commandLine)
public abstract class SymbolResult
public System.Collections.Generic.IReadOnlyList<SymbolResult> Children { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using FluentAssertions;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.CommandLine.Tests.Utility;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
using Process = System.Diagnostics.Process;

Expand Down
64 changes: 64 additions & 0 deletions src/System.CommandLine.Tests/Invocation/InvocationContextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using FluentAssertions;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Threading;
using Xunit;

namespace System.CommandLine.Tests.Invocation
{
public class InvocationContextTests
{
[Fact]
public void InvocationContext_with_cancellation_token_returns_it()
{
using CancellationTokenSource cts = new();
var parseResult = new CommandLineBuilder(new RootCommand())
.Build()
.Parse("");
using InvocationContext context = new(parseResult, cancellationToken: cts.Token);

var token = context.GetCancellationToken();

token.IsCancellationRequested.Should().BeFalse();
cts.Cancel();
token.IsCancellationRequested.Should().BeTrue();
}

[Fact]
public void InvocationContext_with_linked_cancellation_token_can_cancel_by_passed_token()
{
using CancellationTokenSource cts1 = new();
using CancellationTokenSource cts2 = new();
var parseResult = new CommandLineBuilder(new RootCommand())
.Build()
.Parse("");
using InvocationContext context = new(parseResult, cancellationToken: cts1.Token);
context.LinkToken(cts2.Token);

var token = context.GetCancellationToken();

token.IsCancellationRequested.Should().BeFalse();
cts1.Cancel();
token.IsCancellationRequested.Should().BeTrue();
}

[Fact]
public void InvocationContext_with_linked_cancellation_token_can_cancel_by_linked_token()
{
using CancellationTokenSource cts1 = new();
using CancellationTokenSource cts2 = new();
var parseResult = new CommandLineBuilder(new RootCommand())
.Build()
.Parse("");
using InvocationContext context = new(parseResult, cancellationToken: cts1.Token);
context.LinkToken(cts2.Token);

var token = context.GetCancellationToken();

token.IsCancellationRequested.Should().BeFalse();
cts2.Cancel();
token.IsCancellationRequested.Should().BeTrue();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -149,5 +151,28 @@ public void RootCommand_Invoke_can_set_custom_result_code()

resultCode.Should().Be(123);
}

[Fact]
public async Task Command_InvokeAsync_with_cancelation_token_invokes_command_handler()
{
CancellationTokenSource cts = new();
var command = new Command("test");
command.SetHandler((InvocationContext context) =>
{
CancellationToken cancellationToken = context.GetCancellationToken();
Assert.True(cancellationToken.CanBeCanceled);
if (cancellationToken.IsCancellationRequested)
{
return Task.FromResult(42);
}

return Task.FromResult(0);
});

cts.Cancel();
int rv = await command.InvokeAsync("test", cancellationToken: cts.Token);

rv.Should().Be(42);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CommandLine.Help;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.CommandLine.Parsing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -44,7 +46,7 @@ public async Task InvokeAsync_chooses_the_appropriate_command()

var parser = new CommandLineBuilder(new RootCommand
{
first,
first,
second
})
.Build();
Expand Down Expand Up @@ -327,5 +329,39 @@ public async Task When_help_builder_factory_is_specified_it_is_used_to_create_th
handlerWasCalled.Should().BeTrue();
factoryWasCalled.Should().BeTrue();
}

[Fact]
public async Task Command_InvokeAsync_can_cancel_from_middleware()
{
var handlerWasCalled = false;
var isCancelRequested = false;

var command = new Command("the-command");
command.SetHandler((InvocationContext context) =>
{
handlerWasCalled = true;
isCancelRequested = context.GetCancellationToken().IsCancellationRequested;
return Task.FromResult(0);
});


using CancellationTokenSource cts = new();
var parser = new CommandLineBuilder(new RootCommand
{
command
})
.AddMiddleware(async (context, next) =>
{
context.LinkToken(cts.Token);
cts.Cancel();
await next(context);
})
.Build();

await parser.InvokeAsync("the-command");

handlerWasCalled.Should().BeTrue();
isCancelRequested.Should().BeTrue();
}
}
}
11 changes: 6 additions & 5 deletions src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ public void When_parsing_a_string_array_input_then_a_full_path_to_an_executable_

command.Parse(Split("inner -x hello")).Errors.Should().BeEmpty();

command.Parse(Split($"{RootCommand.ExecutablePath} inner -x hello"))
.Errors
.Should()
.ContainSingle(e => e.Message == $"{LocalizationResources.Instance.UnrecognizedCommandOrArgument(RootCommand.ExecutablePath)}");
var parserResult = command.Parse(Split($"\"{RootCommand.ExecutablePath}\" inner -x hello"));
parserResult
.Errors
.Should()
.ContainSingle(e => e.Message == LocalizationResources.Instance.UnrecognizedCommandOrArgument(RootCommand.ExecutablePath));
}

[Fact]
Expand Down Expand Up @@ -76,7 +77,7 @@ public void When_parsing_an_unsplit_string_then_input_a_full_path_to_an_executab
}
};

var result2 = command.Parse($"{RootCommand.ExecutablePath} inner -x hello");
var result2 = command.Parse($"\"{RootCommand.ExecutablePath}\" inner -x hello");

result2.RootCommandResult.Token.Value.Should().Be(RootCommand.ExecutablePath);
}
Expand Down
2 changes: 0 additions & 2 deletions src/System.CommandLine/Binding/BindingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;

#nullable enable

namespace System.CommandLine.Binding
{
/// <summary>
Expand Down
Loading