-
-
Notifications
You must be signed in to change notification settings - Fork 268
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
Add WithExitCondition feature #242
Changes from all commits
f0e7778
6892a7e
31529e1
3aca5e3
a1dbfd8
2505aa5
1c8f829
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
using System; | ||
using System.Diagnostics; | ||
using System.Threading.Tasks; | ||
using CliFx; | ||
using CliFx.Attributes; | ||
using CliFx.Infrastructure; | ||
|
||
namespace CliWrap.Tests.Dummy.Commands; | ||
|
||
[Command("run process")] | ||
public class RunProcessCommand : ICommand | ||
{ | ||
[CommandOption("path")] | ||
public string FilePath { get; init; } = string.Empty; | ||
|
||
[CommandOption("arguments")] | ||
public string Arguments { get; init; } = string.Empty; | ||
|
||
public ValueTask ExecuteAsync(IConsole console) | ||
{ | ||
var startInfo = new ProcessStartInfo | ||
{ | ||
FileName = FilePath, | ||
Arguments = Arguments, | ||
RedirectStandardInput = true, | ||
RedirectStandardOutput = true, | ||
RedirectStandardError = true, | ||
UseShellExecute = false, | ||
CreateNoWindow = true | ||
}; | ||
|
||
var process = new Process(); | ||
process.StartInfo = startInfo; | ||
process.Start(); | ||
|
||
console.Output.WriteLine(process.Id); | ||
|
||
return default; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using System; | ||
using System.Diagnostics; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using FluentAssertions; | ||
using Xunit; | ||
|
||
namespace CliWrap.Tests; | ||
|
||
public class ExitConditionSpecs() | ||
{ | ||
[Fact(Timeout = 15000)] | ||
public async Task I_can_execute_a_command_that_creates_child_process_reusing_standard_output_and_finish_after_child_process_exits() | ||
{ | ||
// Arrange | ||
var cmd = Cli.Wrap(Dummy.Program.FilePath) | ||
.WithArguments( | ||
[ | ||
"run", | ||
"process", | ||
"--path", | ||
Dummy.Program.FilePath, | ||
"--arguments", | ||
"sleep 00:00:03" | ||
] | ||
) | ||
.WithStandardOutputPipe(PipeTarget.ToDelegate(_ => { })) | ||
.WithStandardErrorPipe( | ||
PipeTarget.ToDelegate(line => Console.WriteLine($"Error: {line}")) | ||
); | ||
|
||
// Act | ||
var executionStart = DateTime.UtcNow; | ||
var result = await cmd.ExecuteAsync(); | ||
var executionFinish = DateTime.UtcNow; | ||
|
||
// Assert | ||
executionFinish | ||
.Subtract(executionStart) | ||
.Should() | ||
.BeGreaterThanOrEqualTo(TimeSpan.FromSeconds(3)); | ||
Comment on lines
+38
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably a more reliable way to test this would be to read the stdout. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately it's not possible. The main process is finished almost instantly, and there is nothing we can do to catch the output after that. We can handle the output and wait for for child process to be finished, but then it breaks the whole point of doing this test, where we are interested in specific scenario when parent triggers child and then dies instantly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant to test the current behavior ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only if we make main process to wait for the child. In the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general it seems difficult to create a single test that works reliably on all environments including .NET framework and Linux. In Linux, we should somehow use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought the issue is that, regardless of whether the child process waits for the grandchild or not, because the output streams are inherited and CliWrap waits for them to close, the command effectively finishes when all grandchildren exit. |
||
} | ||
|
||
[Fact(Timeout = 15000)] | ||
public async Task I_can_execute_a_command_that_creates_child_process_resuing_standard_output_and_finish_instantly_after_main_process_exits() | ||
{ | ||
// Arrange | ||
int childProcessId = -1; | ||
var cmd = Cli.Wrap(Dummy.Program.FilePath) | ||
.WithArguments( | ||
[ | ||
"run", | ||
"process", | ||
"--path", | ||
Dummy.Program.FilePath, | ||
"--arguments", | ||
"sleep 00:00:03" | ||
] | ||
) | ||
.WithStandardOutputPipe( | ||
PipeTarget.ToDelegate(line => int.TryParse(line, out childProcessId)) | ||
) | ||
.WithStandardErrorPipe( | ||
PipeTarget.ToDelegate(line => Console.WriteLine($"Error: {line}")) | ||
) | ||
.WithExitCondition(CommandExitCondition.ProcessExited); | ||
|
||
// Act | ||
var executionStart = DateTime.UtcNow; | ||
var result = await cmd.ExecuteAsync(); | ||
var executionFinish = DateTime.UtcNow; | ||
|
||
var process = Process.GetProcessById(childProcessId); | ||
|
||
// Assert | ||
executionFinish.Subtract(executionStart).Should().BeLessThan(TimeSpan.FromSeconds(3)); | ||
|
||
process.HasExited.Should().BeFalse(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace CliWrap; | ||
|
||
/// <summary> | ||
/// Strategy used for identifying the end of command exectuion. | ||
/// </summary> | ||
public enum CommandExitCondition | ||
{ | ||
/// <summary> | ||
/// Command execution is considered finished when the process exits and all standard input and output streams are closed. | ||
/// </summary> | ||
PipesClosed = 0, | ||
|
||
/// <summary> | ||
/// Command execution is considered finished when the process exits, even if the process's standard input and output streams are still open, | ||
/// for example after being inherited by a grandchild process. | ||
/// </summary> | ||
ProcessExited = 1 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
executionStart
/executionFinish
is already provided byresult
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but finish time is a different one. CliWrap sets
ExitTime
when the main process ends. When there are child processes attached to output,ExecuteAsync()
is blocked until all children are completed. In this test, we want to measure time when theExecuteAsync()
was blocked for, so that we can verify whether it was waiting for child or not.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exit time is set when the child process exits:
CliWrap/CliWrap/Utils/ProcessEx.cs
Lines 47 to 51 in 1cfa50e
It shouldn't be affected by the exit condition you've added, since that only affects whether CliWrap waits for pipes to clear or not.
Because your
var executionFinish = ...
statement appears rightawait cmd.ExecuteAsync()
, it's essentially the same (slightly later) timestamp as the one provided byExecutionResult
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CliWrap's ExitTime is always the same no matter what
ExitCondition
is set. So this test would basically always pass.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused in that case. Wouldn't
ExitCondition.ProcessExit
make the command finish earlier, assuming it spawns a grandchild process?