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

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

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Spectre.Console.Cli;
internal abstract class AsyncOutputCommand<T> : AsyncCommand<T> where T : OutputCommandSettings
{
}
internal abstract class OutputCommandSettings : CommandSettings
{
[CommandOption("--target")] public OutputTarget Target { get; init; } = OutputTarget.StdOut;
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<RootNamespace>Geekeey.Process</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" PrivateAssets="compile" />
<PackageReference Include="Spectre.Console.Cli" PrivateAssets="compile" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,42 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
internal sealed class Output : IDisposable
{
private readonly CancellationTokenSource _cts = new();
public Output()
{
Console.CancelKeyPress += Cancel;
}
public StreamReader Stdin { get; } = new(Console.OpenStandardInput(), leaveOpen: false);
public StreamWriter Stdout { get; } = new(Console.OpenStandardOutput(), leaveOpen: false);
public StreamWriter Stderr { get; } = new(Console.OpenStandardError(), leaveOpen: false);
public CancellationToken CancellationToken => _cts.Token;
public static Output Connect()
{
return new Output();
}
private void Cancel(object? sender, ConsoleCancelEventArgs args)
{
args.Cancel = true;
_cts.Cancel();
}
public void Dispose()
{
Stdout.BaseStream.Flush();
Stdout.Dispose();
Stderr.BaseStream.Flush();
Stderr.Dispose();
Stdin.Dispose();
Console.CancelKeyPress -= Cancel;
_cts.Dispose();
}
}

View file

@ -0,0 +1,26 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
[Flags]
internal enum OutputTarget
{
StdOut = 1,
StdErr = 2,
All = StdOut | StdErr
}
internal static class OutputTargetExtensions
{
public static IEnumerable<StreamWriter> GetWriters(this Output output, OutputTarget target)
{
if (target.HasFlag(OutputTarget.StdOut))
{
yield return output.Stdout;
}
if (target.HasFlag(OutputTarget.StdErr))
{
yield return output.Stderr;
}
}
}

View file

@ -0,0 +1,45 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Reflection;
using System.Runtime.InteropServices;
using Spectre.Console.Cli;
namespace Geekeey.Process.Testing.Fixture;
public static class Program
{
private static readonly string? FileExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "exe" : null;
#pragma warning disable IL3000 // only for testing where we don't run in single files!
private static readonly string AssemblyPath = Assembly.GetExecutingAssembly().Location;
#pragma warning restore IL3000
public static string FilePath { get; } = Path.ChangeExtension(AssemblyPath, FileExtension);
private static Task<int> Main(string[] args)
{
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "false");
var app = new CommandApp();
app.Configure(Configuration);
return app.RunAsync(args);
static void Configuration(IConfigurator configuration)
{
configuration.AddCommand<EchoCommand>("echo");
configuration.AddCommand<EchoStdinCommand>("echo-stdin");
configuration.AddCommand<EnvironmentCommand>("env");
configuration.AddCommand<WorkingDirectoryCommand>("cwd");
configuration.AddCommand<WorkingDirectoryCommand>("cwd");
configuration.AddCommand<ExitCommand>("exit");
configuration.AddCommand<LengthCommand>("length");
configuration.AddCommand<SleepCommand>("sleep");
configuration.AddBranch("generate", static generate =>
{
generate.AddCommand<GenerateBlobCommand>("blob");
generate.AddCommand<GenerateClobCommand>("clob");
});
}
}
}

View file

@ -0,0 +1,25 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Spectre.Console.Cli;
internal sealed class EchoCommand : AsyncOutputCommand<EchoCommand.Settings>
{
public sealed class Settings : OutputCommandSettings
{
[CommandOption("--separator <char>")] public string Separator { get; init; } = " ";
[CommandArgument(0, "[line]")] public string[] Items { get; init; } = [];
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
foreach (var writer in output.GetWriters(settings.Target))
{
await writer.WriteLineAsync(string.Join(settings.Separator, settings.Items));
}
return 0;
}
}

View file

@ -0,0 +1,41 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Buffers;
using Spectre.Console.Cli;
internal sealed class EchoStdinCommand : AsyncOutputCommand<EchoStdinCommand.Settings>
{
public sealed class Settings : OutputCommandSettings
{
[CommandOption("--length")] public long Length { get; init; } = long.MaxValue;
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
using var buffer = MemoryPool<byte>.Shared.Rent(81920);
var count = 0L;
while (count < settings.Length)
{
var bytesWanted = (int)Math.Min(buffer.Memory.Length, settings.Length - count);
var bytesRead = await output.Stdin.BaseStream.ReadAsync(buffer.Memory[..bytesWanted], cancellationToken);
if (bytesRead <= 0)
{
break;
}
foreach (var writer in output.GetWriters(settings.Target))
{
await writer.BaseStream.WriteAsync(buffer.Memory[..bytesRead], cancellationToken);
}
count += bytesRead;
}
return 0;
}
}

View file

@ -0,0 +1,29 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Spectre.Console.Cli;
internal sealed class EnvironmentCommand : AsyncOutputCommand<EnvironmentCommand.Settings>
{
public sealed class Settings : OutputCommandSettings
{
[CommandArgument(0, "<ARGUMENT>")] public string[] Variables { get; init; } = [];
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
foreach (var name in settings.Variables)
{
var value = Environment.GetEnvironmentVariable(name) ?? string.Empty;
foreach (var writer in output.GetWriters(settings.Target))
{
await writer.WriteLineAsync(value);
}
}
return 0;
}
}

View file

@ -0,0 +1,21 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Spectre.Console.Cli;
internal sealed class ExitCommand : AsyncCommand<ExitCommand.Settings>
{
public sealed class Settings : CommandSettings
{
[CommandArgument(1, "<code>")] public int Code { get; init; }
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
await output.Stderr.WriteLineAsync($"Exit code set to {settings.Code}");
return settings.Code;
}
}

View file

@ -0,0 +1,40 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Buffers;
using Spectre.Console.Cli;
internal sealed class GenerateBlobCommand : AsyncOutputCommand<GenerateBlobCommand.Settings>
{
private readonly Random _random = new(1234567);
public sealed class Settings : OutputCommandSettings
{
[CommandOption("--length")] public long Length { get; init; } = 100_000;
[CommandOption("--buffer")] public int BufferSize { get; init; } = 1024;
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
using var bytes = MemoryPool<byte>.Shared.Rent(settings.BufferSize);
var total = 0L;
while (total < settings.Length)
{
_random.NextBytes(bytes.Memory.Span);
var count = (int)Math.Min(bytes.Memory.Length, settings.Length - total);
foreach (var writer in output.GetWriters(settings.Target))
{
await writer.BaseStream.WriteAsync(bytes.Memory[..count], cancellationToken);
}
total += count;
}
return 0;
}
}

View file

@ -0,0 +1,42 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Text;
using Spectre.Console.Cli;
internal sealed class GenerateClobCommand : AsyncOutputCommand<GenerateClobCommand.Settings>
{
private readonly Random _random = new(1234567);
private readonly char[] _chars = [.. Enumerable.Range(32, 94).Select(i => (char)i)];
public sealed class Settings : OutputCommandSettings
{
[CommandOption("--length")] public int Length { get; init; } = 100_000;
[CommandOption("--lines")] public int LinesCount { get; init; } = 1;
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
var buffer = new StringBuilder(settings.Length);
for (var line = 0; line < settings.LinesCount; line++)
{
buffer.Clear();
for (var i = 0; i < settings.Length; i++)
{
buffer.Append(_chars[_random.Next(0, _chars.Length)]);
}
foreach (var writer in output.GetWriters(settings.Target))
{
await writer.WriteLineAsync(buffer.ToString());
}
}
return 0;
}
}

View file

@ -0,0 +1,40 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Buffers;
using System.Globalization;
using Spectre.Console.Cli;
internal sealed class LengthCommand : AsyncOutputCommand<LengthCommand.Settings>
{
public sealed class Settings : OutputCommandSettings
{
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
using var buffer = MemoryPool<byte>.Shared.Rent(81920);
var count = 0L;
while (true)
{
var bytesRead = await output.Stdin.BaseStream.ReadAsync(buffer.Memory, cancellationToken);
if (bytesRead <= 0)
{
break;
}
count += bytesRead;
}
foreach (var writer in output.GetWriters(settings.Target))
{
await writer.WriteLineAsync(count.ToString(CultureInfo.InvariantCulture));
}
return 0;
}
}

View file

@ -0,0 +1,34 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Spectre.Console.Cli;
internal sealed class SleepCommand : AsyncCommand<SleepCommand.Settings>
{
public sealed class Settings : CommandSettings
{
[CommandArgument(0, "[duration]")] public TimeSpan Duration { get; init; } = TimeSpan.FromSeconds(1);
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
try
{
await Console.Out.WriteLineAsync($"Sleeping for {settings.Duration}...");
await Console.Out.FlushAsync(CancellationToken.None);
await Task.Delay(settings.Duration, output.CancellationToken);
}
catch (OperationCanceledException)
{
await Console.Out.WriteLineAsync("Canceled.");
await Console.Out.FlushAsync(CancellationToken.None);
}
await Console.Out.WriteLineAsync("Done.");
await Console.Out.FlushAsync(CancellationToken.None);
return 0;
}
}

View file

@ -0,0 +1,23 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Spectre.Console.Cli;
internal sealed class WorkingDirectoryCommand : AsyncOutputCommand<WorkingDirectoryCommand.Settings>
{
public sealed class Settings : OutputCommandSettings
{
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
using var output = Output.Connect();
foreach (var writer in output.GetWriters(settings.Target))
{
await writer.WriteLineAsync(Directory.GetCurrentDirectory());
}
return 0;
}
}

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,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<RootNamespace>Geekeey.Process</RootNamespace>
</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");
}
}
}

Some files were not shown because too many files have changed in this diff Show more