build: initial project release
All checks were successful
release / dotnet-release-workflow (push) Successful in 1m23s

This commit is contained in:
Louis Seubert 2026-01-20 22:41:16 +01:00
commit 48c483c568
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
62 changed files with 4957 additions and 0 deletions

View file

@ -0,0 +1,6 @@
[*.{cs,vb}]
# disable IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = none
# disable IDE0005: Unnecessary using directive
dotnet_diagnostic.IDE0005.severity = none

View file

@ -0,0 +1,181 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Text;
using Geekeey.Process.Buffered;
namespace Geekeey.Process.Tests;
internal sealed class CancellationTests
{
private static Action<string> NotifyOnStart(out TaskCompletionSource tcs)
{
// run the continuation async on the thread pool to allow the io reader to complete
var source = tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
return line =>
{
if (line.Contains("Sleeping for", StringComparison.OrdinalIgnoreCase))
{
source.TrySetResult();
}
};
}
[Test]
public async Task I_can_execute_a_command_and_cancel_it_immediately()
{
// Arrange
using var cts = new CancellationTokenSource();
var stdout = new StringBuilder();
var target = PipeTarget.Merge(
PipeTarget.ToDelegate(NotifyOnStart(out var tcs)),
PipeTarget.ToStringBuilder(stdout)
);
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["sleep", "00:00:30"]) |
target;
// Act
var task = cmd.ExecuteAsync(cts.Token);
await tcs.Task;
await cts.CancelAsync();
// Assert
await Assert.That(async () => await task).Throws<OperationCanceledException>();
using (Assert.Multiple())
{
await Assert.That(ProcessTree.HasExited(task.ProcessId)).IsTrue();
await Assert.That(stdout.ToString()).Contains("Sleeping for");
await Assert.That(stdout.ToString()).DoesNotContain("Done.");
}
}
[Test]
public async Task I_can_execute_a_command_and_kill_it_immediately()
{
// Arrange
var stdout = new StringBuilder();
var target = PipeTarget.Merge(
PipeTarget.ToDelegate(NotifyOnStart(out var tcs)),
PipeTarget.ToStringBuilder(stdout)
);
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["sleep", "00:00:30"]) |
target;
// Act
var task = cmd.ExecuteAsync();
await tcs.Task;
task.Kill();
// Assert
await Assert.That(async () => await task).Throws<CommandExecutionException>();
using (Assert.Multiple())
{
await Assert.That(ProcessTree.HasExited(task.ProcessId)).IsTrue();
await Assert.That(stdout.ToString()).Contains("Sleeping for");
await Assert.That(stdout.ToString()).DoesNotContain("Done.");
}
}
[Test]
public async Task I_can_execute_a_command_with_buffering_and_kill_it_immediately()
{
// Arrange
var stdout = new StringBuilder();
var target = PipeTarget.Merge(
PipeTarget.ToDelegate(NotifyOnStart(out var tcs)),
PipeTarget.ToStringBuilder(stdout)
);
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["sleep", "00:00:30"]) |
target;
// Act
var task = cmd.ExecuteBufferedAsync();
await tcs.Task;
task.Kill();
// Assert
await Assert.That(async () => await task).Throws<CommandExecutionException>();
using (Assert.Multiple())
{
await Assert.That(ProcessTree.HasExited(task.ProcessId)).IsTrue();
await Assert.That(stdout.ToString()).Contains("Sleeping for");
await Assert.That(stdout.ToString()).DoesNotContain("Done.");
}
}
[Test]
public async Task I_can_execute_a_command_and_interrupt_it_immediately()
{
// Arrange
var stdout = new StringBuilder();
var target = PipeTarget.Merge(
PipeTarget.ToDelegate(NotifyOnStart(out var tcs)),
PipeTarget.ToStringBuilder(stdout)
);
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["sleep", "00:00:30"]) |
target;
// Act
var task = cmd.ExecuteAsync();
await tcs.Task;
task.Interrupt();
// Assert
await Assert.That(async () => await task).ThrowsNothing();
using (Assert.Multiple())
{
await Assert.That(ProcessTree.HasExited(task.ProcessId)).IsTrue();
await Assert.That(stdout.ToString()).Contains("Sleeping for");
await Assert.That(stdout.ToString()).Contains("Done.");
}
}
[Test]
public async Task I_can_execute_a_command_with_buffering_and_interrupt_it_immediately()
{
// Arrange
var stdout = new StringBuilder();
var target = PipeTarget.Merge(
PipeTarget.ToDelegate(NotifyOnStart(out var tcs)),
PipeTarget.ToStringBuilder(stdout)
);
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["sleep", "00:00:30"]) |
target;
// Act
var task = cmd.ExecuteBufferedAsync();
await tcs.Task;
task.Interrupt();
// Assert
await Assert.That(async () => await task).ThrowsNothing();
using (Assert.Multiple())
{
await Assert.That(ProcessTree.HasExited(task.ProcessId)).IsTrue();
await Assert.That(stdout.ToString()).Contains("Sleeping for");
await Assert.That(stdout.ToString()).Contains("Done.");
}
}
}

View file

@ -0,0 +1,273 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Process.Tests;
internal sealed class CommandTests
{
[Test]
public async Task I_can_create_a_command_with_the_default_configuration()
{
var cmd = new Command("foo");
using (Assert.Multiple())
{
await Assert.That(cmd.TargetFilePath).IsEqualTo("foo");
await Assert.That(cmd.Arguments).IsEmpty();
await Assert.That(cmd.WorkingDirPath).IsEqualTo(Directory.GetCurrentDirectory());
await Assert.That(cmd.Environment).IsEmpty();
await Assert.That(cmd.Validation).HasFlag(ValidationMode.ZeroExitCode);
await Assert.That(cmd.StandardInputPipe).IsEqualTo(PipeSource.Null);
await Assert.That(cmd.StandardOutputPipe).IsEqualTo(PipeTarget.Null);
await Assert.That(cmd.StandardErrorPipe).IsEqualTo(PipeTarget.Null);
}
}
[Test]
public async Task I_can_configure_the_target_file()
{
var cmd = new Command("foo");
var modified = cmd.WithTargetFile("bar");
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo("bar");
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.TargetFilePath).IsNotEqualTo("bar");
}
}
[Test]
public async Task I_can_configure_the_command_line_arguments()
{
var cmd = new Command("foo").WithArguments("xxx");
var modified = cmd.WithArguments("abc def");
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo("abc def");
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.Arguments).IsNotEqualTo("abc def");
}
}
[Test]
public async Task I_can_configure_the_command_line_arguments_by_passing_an_array()
{
var cmd = new Command("foo").WithArguments("xxx");
var modified = cmd.WithArguments(["abc", "def"]);
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo("abc def");
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.Arguments).IsNotEqualTo("abc def");
}
}
[Test]
public async Task I_can_configure_the_command_line_arguments_using_a_builder()
{
var cmd = new Command("foo").WithArguments("xxx");
var modified = cmd.WithArguments(args => args
.Add("-a")
.Add("foo bar")
.Add("\"foo\\\\bar\"")
.Add(3.14)
.Add(["foo", "bar"])
.Add([-10, 12.12]));
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo("-a \"foo bar\" \"\\\"foo\\\\bar\\\"\" 3.14 foo bar -10 12.12");
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.Arguments).IsNotEqualTo("-a \"foo bar\" \"\\\"foo\\\\bar\\\"\" 3.14 foo bar -10 12.12");
}
}
[Test]
public async Task I_can_configure_the_working_directory()
{
var cmd = new Command("foo").WithWorkingDirectory("xxx");
var modified = cmd.WithWorkingDirectory("new");
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo("new");
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.WorkingDirPath).IsNotEqualTo("new");
}
}
[Test]
public async Task I_can_configure_the_environment_variables()
{
var cmd = new Command("foo").WithEnvironment(e => e.Set("xxx", "xxx"));
var vars = new Dictionary<string, string?>
{
["name"] = "value",
["key"] = "door",
};
var modified = cmd.WithEnvironment(vars);
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(vars);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.Environment).IsNotEqualTo(vars);
}
}
[Test]
public async Task I_can_configure_the_environment_variables_using_a_builder()
{
var cmd = new Command("foo").WithEnvironment(e => e.Set("xxx", "xxx"));
var modified = cmd.WithEnvironment(env => env
.Set("name", "value")
.Set("key", "door")
.Set(new Dictionary<string, string?>
{
["zzz"] = "yyy",
["aaa"] = "bbb",
}));
using (Assert.Multiple())
{
var vars = new Dictionary<string, string?>
{
["name"] = "value",
["key"] = "door",
["zzz"] = "yyy",
["aaa"] = "bbb",
};
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEquivalentTo(vars);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.Environment).IsNotEqualTo(vars);
}
}
[Test]
public async Task I_can_configure_the_result_validation_strategy()
{
var cmd = new Command("foo").WithExitValidation(ValidationMode.ZeroExitCode);
var modified = cmd.WithExitValidation(ValidationMode.None);
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(ValidationMode.None);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.Validation).IsNotEqualTo(ValidationMode.None);
}
}
[Test]
public async Task I_can_configure_the_stdin_pipe()
{
var cmd = new Command("foo").WithStandardInputPipe(PipeSource.Null);
var pipeSource = PipeSource.FromStream(Stream.Null);
var modified = cmd.WithStandardInputPipe(pipeSource);
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(pipeSource);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.StandardInputPipe).IsNotEqualTo(pipeSource);
}
}
[Test]
public async Task I_can_configure_the_stdout_pipe()
{
var cmd = new Command("foo").WithStandardOutputPipe(PipeTarget.Null);
var pipeTarget = PipeTarget.ToStream(Stream.Null);
var modified = cmd.WithStandardOutputPipe(pipeTarget);
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(pipeTarget);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(cmd.StandardErrorPipe);
await Assert.That(cmd.StandardOutputPipe).IsNotEqualTo(pipeTarget);
}
}
[Test]
public async Task I_can_configure_the_stderr_pipe()
{
var cmd = new Command("foo").WithStandardErrorPipe(PipeTarget.Null);
var pipeTarget = PipeTarget.ToStream(Stream.Null);
var modified = cmd.WithStandardErrorPipe(pipeTarget);
using (Assert.Multiple())
{
await Assert.That(modified.TargetFilePath).IsEqualTo(cmd.TargetFilePath);
await Assert.That(modified.Arguments).IsEqualTo(cmd.Arguments);
await Assert.That(modified.WorkingDirPath).IsEqualTo(cmd.WorkingDirPath);
await Assert.That(modified.Environment).IsEqualTo(cmd.Environment);
await Assert.That(modified.Validation).IsEqualTo(cmd.Validation);
await Assert.That(modified.StandardInputPipe).IsEqualTo(cmd.StandardInputPipe);
await Assert.That(modified.StandardOutputPipe).IsEqualTo(cmd.StandardOutputPipe);
await Assert.That(modified.StandardErrorPipe).IsEqualTo(pipeTarget);
await Assert.That(cmd.StandardErrorPipe).IsNotEqualTo(pipeTarget);
}
}
}

View file

@ -0,0 +1,139 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Geekeey.Process.Buffered;
namespace Geekeey.Process.Tests;
internal sealed class ExecuteTests
{
[Test]
public async Task I_can_execute_a_command_and_get_the_exit_code_and_execution_time()
{
// Arrange
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["echo"]);
// Act
var result = await cmd.ExecuteAsync();
await Assert.That(result.ExitCode).IsZero();
// Assert
using (Assert.Multiple())
{
await Assert.That(result.ExitCode).IsZero();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.RunTime).IsGreaterThan(TimeSpan.Zero);
}
}
[Test]
public async Task I_can_execute_a_command_and_get_the_associated_process_id()
{
// Arrange
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["echo"]);
// Act
var task = cmd.ExecuteAsync();
// Assert
await Assert.That(task.ProcessId).IsNotZero();
await task;
}
[Test]
public async Task I_can_execute_a_command_with_a_configured_awaiter()
{
// Arrange
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["echo"]);
// Act + Assert
await cmd.ExecuteAsync().ConfigureAwait(false);
}
[Test]
public async Task I_can_try_to_execute_a_command_and_get_an_error_if_the_target_file_does_not_exist()
{
// Arrange
var cmd = new Command("some_exe_with_does_not_exits");
// Act + Assert
await Assert.That(() => cmd.ExecuteAsync()).Throws<InvalidOperationException>()
.WithInnerException();
}
[Test]
public async Task I_can_execute_a_command_with_a_custom_working_directory()
{
// Arrange
using var dir = TestTempDirectory.Create();
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments("cwd")
.WithWorkingDirectory(dir.Path);
// Act
var result = await cmd.ExecuteBufferedAsync();
await Assert.That(result.ExitCode).IsZero();
// Assert
var lines = result.StandardOutput.Split(Environment.NewLine);
await Assert.That(lines).Contains(dir.Path);
}
[Test]
public async Task I_can_execute_a_command_with_additional_environment_variables()
{
// Arrange
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["env", "foo", "bar"])
.WithEnvironment(env => env
.Set("foo", "hello")
.Set("bar", "world"));
// Act
var result = await cmd.ExecuteBufferedAsync();
await Assert.That(result.ExitCode).IsZero();
// Assert
var lines = result.StandardOutput.Split(Environment.NewLine);
await Assert.That(lines).Contains("hello");
await Assert.That(lines).Contains("world");
}
[Test]
public async Task I_can_execute_a_command_with_some_environment_variables_overwritten()
{
// Arrange
var key = Guid.NewGuid();
var variableToKeep = $"GKY_TEST_KEEP_{key}";
var variableToOverwrite = $"GKY_TEST_OVERWRITE_{key}";
var variableToUnset = $"GKY_TEST_UNSET_{key}";
using var a = TestEnvironment.Create(variableToKeep, "keep");
using var b = TestEnvironment.Create(variableToOverwrite, "overwrite");
using var c = TestEnvironment.Create(variableToUnset, "unset");
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["env", variableToKeep, variableToOverwrite, variableToUnset])
.WithEnvironment(env => env
.Set(variableToOverwrite, "overwritten")
.Set(variableToUnset, null));
// Act
var result = await cmd.ExecuteBufferedAsync();
await Assert.That(result.ExitCode).IsZero();
// Assert
var lines = result.StandardOutput.Split(Environment.NewLine);
await Assert.That(lines).Contains("keep");
await Assert.That(lines).Contains("overwritten");
}
}

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\process\Geekeey.Process.csproj" />
<ProjectReference Include="..\process.dummy.app\Geekeey.Process.Dummy.App.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,81 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Process.Tests;
internal sealed class LineBreakTests
{
private static Command Echo()
{
return new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin");
}
[Test]
public async Task I_can_execute_a_command_and_split_the_stdout_by_newline()
{
// Arrange
const string data = "Foo\nBar\nBaz";
var stdOutLines = new List<string>();
var cmd = data | Echo() | stdOutLines.Add;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stdOutLines).IsEquivalentTo(["Foo", "Bar", "Baz"]);
}
[Test]
public async Task I_can_execute_a_command_and_split_the_stdout_by_caret_return()
{
// Arrange
const string data = "Foo\rBar\rBaz";
var stdOutLines = new List<string>();
var cmd = data | Echo() | stdOutLines.Add;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stdOutLines).IsEquivalentTo(["Foo", "Bar", "Baz"]);
}
[Test]
public async Task I_can_execute_a_command_and_split_the_stdout_by_caret_return_followed_by_newline()
{
// Arrange
const string data = "Foo\r\nBar\r\nBaz";
var stdOutLines = new List<string>();
var cmd = data | Echo() | stdOutLines.Add;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stdOutLines).IsEquivalentTo(["Foo", "Bar", "Baz"]);
}
[Test]
public async Task I_can_execute_a_command_and_split_the_stdout_by_newline_while_including_empty_lines()
{
// Arrange
const string data = "Foo\r\rBar\n\nBaz";
var stdOutLines = new List<string>();
var cmd = data | Echo() | stdOutLines.Add;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stdOutLines).IsEquivalentTo(["Foo", "", "Bar", "", "Baz"]);
}
}

View file

@ -0,0 +1,50 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Geekeey.Process.Buffered;
namespace Geekeey.Process.Tests;
internal sealed class PathResolutionTests
{
[Test]
public async Task I_can_execute_a_command_on_an_executable_using_its_short_name()
{
// Arrange
var cmd = new Command("dotnet")
.WithArguments("--version");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(result.ExitCode).IsEqualTo(0);
await Assert.That(result.StandardOutput.Trim()).Matches(@"^\d+\.\d+\.\d+$");
}
}
[Test]
[Platform(PlatformAttribute.Windows)]
public async Task I_can_execute_a_command_on_a_script_using_its_short_name()
{
// Arrange
using var dir = TestTempDirectory.Create();
await File.WriteAllTextAsync(Path.Combine(dir.Path, "script.cmd"), "@echo hi");
using var _1 = TestEnvironment.ExtendPath(dir.Path);
var cmd = new Command("script");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
// Assert
using (Assert.Multiple())
{
await Assert.That(result.ExitCode).IsEqualTo(0);
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("hi");
}
}
}

View file

@ -0,0 +1,536 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Text;
using Geekeey.Process.Buffered;
namespace Geekeey.Process.Tests;
internal sealed class PipingTests
{
#region Stdin
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_an_async_anonymous_source()
{
// Arrange
var source = PipeSource.Create(async (destination, cancellationToken)
=> await destination.WriteAsync("Hello World!"u8.ToArray(), cancellationToken));
var cmd = source |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("Hello World!");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_a_sync_anonymous_source()
{
// Arrange
var source = PipeSource.Create(destination
=> destination.Write("Hello World!"u8.ToArray()));
var cmd = source |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("Hello World!");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_a_stream()
{
// Arrange
using var source = new MemoryStream("Hello World!"u8.ToArray());
var cmd = source |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("Hello World!");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_memory()
{
// Arrange
var data = new ReadOnlyMemory<byte>("Hello World!"u8.ToArray());
var cmd = data |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("Hello World!");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_a_byte_array()
{
// Arrange
var data = "Hello World!"u8.ToArray();
var cmd = data |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("Hello World!");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_a_string()
{
// Arrange
var data = "Hello World!";
var cmd = data |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("Hello World!");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_another_command()
{
// Arrange
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "blob", "--length", "100000"]) |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("length");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("100000");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_a_chain_of_commands()
{
// Arrange
var cmd =
"Hello world" |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("echo-stdin") |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["echo-stdin", "--length", "5"]) |
new Command(Testing.Fixture.Program.FilePath)
.WithArguments("length");
// Act
var result = await cmd.ExecuteBufferedAsync();
// Assert
await Assert.That(result.StandardOutput.Trim()).IsEqualTo("5");
}
#endregion
#region Stdout
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_an_async_anonymous_target()
{
// Arrange
using var stream = new MemoryStream();
var target = PipeTarget.Create(async (origin, cancellationToken) =>
// ReSharper disable once AccessToDisposedClosure
await origin.CopyToAsync(stream, cancellationToken)
);
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "blob", "--length", "100000"]) |
target;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stream.Length).IsEqualTo(100_000);
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_a_sync_anonymous_target()
{
// Arrange
using var stream = new MemoryStream();
var target = PipeTarget.Create(origin =>
// ReSharper disable once AccessToDisposedClosure
origin.CopyTo(stream)
);
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "blob", "--length", "100000"]) |
target;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stream.Length).IsEqualTo(100_000);
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_a_stream()
{
// Arrange
using var stream = new MemoryStream();
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "blob", "--length", "100000"]) |
stream;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stream.Length).IsEqualTo(100_000);
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_a_string_builder()
{
// Arrange
var buffer = new StringBuilder();
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["echo", "Hello World!"]) |
buffer;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(buffer.ToString().Trim()).IsEqualTo("Hello World!");
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_an_async_delegate()
{
// Arrange
var stdOutLinesCount = 0;
async Task HandleStdOutAsync(string line)
{
await Task.Yield();
stdOutLinesCount++;
}
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "clob", "--lines", "100"]) |
HandleStdOutAsync;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stdOutLinesCount).IsEqualTo(100);
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_an_async_delegate_with_cancellation()
{
// Arrange
var stdOutLinesCount = 0;
async Task HandleStdOutAsync(string line, CancellationToken cancellationToken = default)
{
await Task.Delay(1, cancellationToken);
stdOutLinesCount++;
}
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "clob", "--lines", "100"]) |
HandleStdOutAsync;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stdOutLinesCount).IsEqualTo(100);
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_a_sync_delegate()
{
// Arrange
var stdOutLinesCount = 0;
void HandleStdOut(string line)
{
stdOutLinesCount++;
}
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "clob", "--lines", "100"]) |
HandleStdOut;
// Act
await cmd.ExecuteAsync();
// Assert
await Assert.That(stdOutLinesCount).IsEqualTo(100);
}
#endregion
#region Stdout & Stderr
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_and_stderr_into_separate_stream()
{
// Arrange
using var stdOut = new MemoryStream();
using var stdErr = new MemoryStream();
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "blob", "--target", "all", "--length", "100000"]) |
(stdOut, stdErr);
// Act
await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(stdOut.Length).IsEqualTo(100_000);
await Assert.That(stdErr.Length).IsEqualTo(100_000);
}
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_and_stderr_into_string_builder()
{
// Arrange
var stdOutBuffer = new StringBuilder();
var stdErrBuffer = new StringBuilder();
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["echo", "Hello world!", "--target", "all"]) |
(stdOutBuffer, stdErrBuffer);
// Act
await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(stdOutBuffer.ToString().Trim()).IsEqualTo("Hello world!");
await Assert.That(stdErrBuffer.ToString().Trim()).IsEqualTo("Hello world!");
}
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_and_stderr_into_separate_async_delegate()
{
// Arrange
var stdOutLinesCount = 0;
var stdErrLinesCount = 0;
async Task HandleStdOutAsync(string line)
{
await Task.Yield();
stdOutLinesCount++;
}
async Task HandleStdErrAsync(string line)
{
await Task.Yield();
stdErrLinesCount++;
}
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "clob", "--target", "all", "--lines", "100"]) |
(HandleStdOutAsync, HandleStdErrAsync);
// Act
await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(stdOutLinesCount).IsEqualTo(100);
await Assert.That(stdErrLinesCount).IsEqualTo(100);
}
}
[Test]
public async Task
I_can_execute_a_command_and_pipe_the_stdout_and_stderr_into_separate_async_delegate_with_cancellation()
{
// Arrange
var stdOutLinesCount = 0;
var stdErrLinesCount = 0;
async Task HandleStdOutAsync(string line, CancellationToken cancellationToken = default)
{
await Task.Delay(1, cancellationToken);
stdOutLinesCount++;
}
async Task HandleStdErrAsync(string line, CancellationToken cancellationToken = default)
{
await Task.Delay(1, cancellationToken);
stdErrLinesCount++;
}
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "clob", "--target", "all", "--lines", "100"]) |
(HandleStdOutAsync, HandleStdErrAsync);
// Act
await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(stdOutLinesCount).IsEqualTo(100);
await Assert.That(stdErrLinesCount).IsEqualTo(100);
}
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_and_stderr_into_separate_sync_delegate()
{
// Arrange
var stdOutLinesCount = 0;
var stdErrLinesCount = 0;
void HandleStdOut(string line)
{
stdOutLinesCount++;
}
void HandleStdErr(string line)
{
stdErrLinesCount++;
}
var cmd =
new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "clob", "--target", "all", "--lines", "100"]) |
(HandleStdOut, HandleStdErr);
// Act
await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(stdOutLinesCount).IsEqualTo(100);
await Assert.That(stdErrLinesCount).IsEqualTo(100);
}
}
#endregion
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_multiple_targets()
{
// Arrange
using var stream1 = new MemoryStream();
using var stream2 = new MemoryStream();
using var stream3 = new MemoryStream();
var target = PipeTarget.Merge(
PipeTarget.ToStream(stream1),
PipeTarget.ToStream(stream2),
PipeTarget.ToStream(stream3)
);
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "blob", "--length", "100000"]) |
target;
// Act
await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(stream1.Length).IsEqualTo(100_000);
await Assert.That(stream2.Length).IsEqualTo(100_000);
await Assert.That(stream3.Length).IsEqualTo(100_000);
await Assert.That(stream1.ToArray()).IsEquivalentTo(stream2.ToArray());
await Assert.That(stream2.ToArray()).IsEquivalentTo(stream3.ToArray());
}
}
[Test]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_multiple_hierarchical_targets()
{
// Arrange
using var stream1 = new MemoryStream();
using var stream2 = new MemoryStream();
using var stream3 = new MemoryStream();
using var stream4 = new MemoryStream();
var target = PipeTarget.Merge(
PipeTarget.ToStream(stream1),
PipeTarget.Merge(
PipeTarget.ToStream(stream2),
PipeTarget.Merge(
PipeTarget.ToStream(stream3),
PipeTarget.ToStream(stream4))));
var cmd = new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["generate", "blob", "--length", "100000"]) |
target;
// Act
await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(stream1.Length).IsEqualTo(100_000);
await Assert.That(stream2.Length).IsEqualTo(100_000);
await Assert.That(stream3.Length).IsEqualTo(100_000);
await Assert.That(stream4.Length).IsEqualTo(100_000);
await Assert.That(stream1.ToArray()).IsEquivalentTo(stream2.ToArray());
await Assert.That(stream2.ToArray()).IsEquivalentTo(stream3.ToArray());
await Assert.That(stream3.ToArray()).IsEquivalentTo(stream4.ToArray());
}
}
}

View file

@ -0,0 +1,58 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Geekeey.Process.Buffered;
namespace Geekeey.Process.Tests;
internal sealed class ValidationTests
{
private static Command Exit()
{
return new Command(Testing.Fixture.Program.FilePath)
.WithArguments(["exit", "1"]);
}
[Test]
public async Task I_can_try_to_execute_a_command_and_get_an_error_if_it_returns_a_non_zero_exit_code()
{
// Arrange
var cmd = Exit();
// Act & Assert
await Assert.That(async () => await cmd.ExecuteAsync()).Throws<CommandExecutionException>().And
.Member(static exception => exception.Message, static source => source.Contains("a non-zero exit code (1)")).And
.Member(static exception => exception.ExitCode, static source => source.IsEqualTo(1));
}
[Test]
public async Task I_can_try_to_execute_a_command_with_buffering_and_get_a_detailed_error_if_it_returns_a_non_zero_exit_code()
{
// Arrange
var cmd = Exit();
// Act & Assert
await Assert.That(async () => await cmd.ExecuteBufferedAsync()).Throws<CommandExecutionException>().And
.Member(static exception => exception.Message, static source => source.Contains("Exit code set to 1")).And
.Member(static exception => exception.ExitCode, static source => source.IsEqualTo(1));
}
[Test]
public async Task I_can_execute_a_command_without_validating_the_exit_code()
{
// Arrange
var cmd = Exit()
.WithExitValidation(ValidationMode.None);
// Act
var result = await cmd.ExecuteAsync();
// Assert
using (Assert.Multiple())
{
await Assert.That(result.ExitCode).IsEqualTo(1);
await Assert.That(result.IsSuccess).IsFalse();
}
}
}

View file

@ -0,0 +1,35 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Process.Tests;
internal sealed class PlatformAttribute : SkipAttribute
{
// from the OperatingSystem definitions
public const string Browser = "BROWSER";
public const string Wasi = "WASI";
public const string Windows = "WINDOWS";
public const string Osx = "OSX";
public const string MacCatalyst = "MACCATALYST";
public const string Ios = "IOS";
public const string Tvos = "TVOS";
public const string Android = "ANDROID";
public const string Linux = "LINUX";
public const string Freebsd = "FREEBSD";
public const string Netbsd = "NETBSD";
public const string Illumos = "ILLUMOS";
public const string Solaris = "SOLARIS";
private readonly string[] _os;
public PlatformAttribute(params string[] os) : base("Test skipped on unsupported platform.")
{
_os = os;
}
public override Task<bool> ShouldSkip(TestRegisteredContext context)
{
return Task.FromResult(!_os.Any(OperatingSystem.IsOSPlatform));
}
}

View file

@ -0,0 +1,21 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Process.Tests;
internal static class ProcessTree
{
public static bool HasExited(int id)
{
try
{
using var process = System.Diagnostics.Process.GetProcessById(id);
return process.HasExited;
}
catch
{
// GetProcessById throws if the process can not be found, which means it is not running!
return true;
}
}
}

View file

@ -0,0 +1,32 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Process.Tests;
internal sealed class TestEnvironment : IDisposable
{
private readonly Action _action;
private TestEnvironment(Action action)
{
_action = action;
}
public static TestEnvironment Create(string name, string? value)
{
var lastValue = Environment.GetEnvironmentVariable(name);
Environment.SetEnvironmentVariable(name, value);
return new TestEnvironment(() => Environment.SetEnvironmentVariable(name, lastValue));
}
public static TestEnvironment ExtendPath(string path)
{
return Create("PATH", Environment.GetEnvironmentVariable("PATH") + Path.PathSeparator + path);
}
public void Dispose()
{
_action();
}
}

View file

@ -0,0 +1,34 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Process.Tests;
internal sealed class TestTempDirectory : IDisposable
{
private TestTempDirectory(string path)
{
Path = path;
}
public static TestTempDirectory Create()
{
var location = System.Reflection.Assembly.GetExecutingAssembly().Location;
var pwd = System.IO.Path.GetDirectoryName(location) ?? Directory.GetCurrentDirectory();
var dirPath = System.IO.Path.Combine(pwd, "Temp", Guid.NewGuid().ToString());
Directory.CreateDirectory(dirPath);
return new TestTempDirectory(dirPath);
}
public string Path { get; }
public void Dispose()
{
try
{
Directory.Delete(Path, recursive: true);
}
catch (DirectoryNotFoundException) { }
}
}