using System.Diagnostics;
using w4c_workflows.Services.Execution;
using Xunit;
namespace w4c_workflows.Tests;
///
/// Regression tests for subprocess capture and reaping. The runner must bound
/// memory for chatty children and must never leave a child tree alive when the
/// caller's token is cancelled.
///
public class ProcessRunnerTests
{
[Fact]
public async Task ReadCappedAsync_returns_everything_under_the_cap()
{
using var reader = new StringReader("hello world");
var captured = await ProcessRunner.ReadCappedAsync(reader, 100);
Assert.Equal("hello world", captured);
}
[Fact]
public async Task ReadCappedAsync_captures_at_most_the_cap_but_drains_the_reader()
{
// 10x the cap: the reader must be fully drained (so a child never blocks
// on a full pipe) while only the first `cap` characters are retained.
using var reader = new StringReader(new string('a', 1_000));
var captured = await ProcessRunner.ReadCappedAsync(reader, 100);
Assert.Equal(100, captured.Length);
Assert.Equal(new string('a', 100), captured);
}
[Fact]
public async Task RunAsync_throws_timeout_when_the_child_exceeds_the_limit()
{
if (!OperatingSystem.IsLinux())
return;
var ex = await Assert.ThrowsAsync(() => ProcessRunner.RunAsync(
"/bin/sh", new[] { "-c", "sleep 30" }, Path.GetTempPath(),
null, null, TimeSpan.FromMilliseconds(300), default));
Assert.Contains("time limit", ex.Message);
}
[Fact]
public async Task RunAsync_kills_the_child_when_cancelled_externally()
{
if (!OperatingSystem.IsLinux())
return;
var pidFile = Path.Combine(Path.GetTempPath(), $"w4c-proc-{Guid.NewGuid():N}.pid");
using var cts = new CancellationTokenSource();
var run = ProcessRunner.RunAsync(
"/bin/sh",
new[] { "-c", $"echo $$ > '{pidFile}'; exec sleep 30" },
Path.GetTempPath(),
null, null, TimeSpan.FromSeconds(30), cts.Token);
var deadline = DateTime.UtcNow.AddSeconds(5);
while (!File.Exists(pidFile) && DateTime.UtcNow < deadline)
await Task.Delay(20);
Assert.True(File.Exists(pidFile), "child never reported its pid");
cts.Cancel();
await Assert.ThrowsAnyAsync(() => run);
var pid = int.Parse(File.ReadAllText(pidFile).Trim());
var gone = false;
for (var i = 0; i < 100 && !gone; i++)
{
gone = !IsAlive(pid);
if (!gone)
await Task.Delay(50);
}
Assert.True(gone, $"child process {pid} survived external cancellation");
}
private static bool IsAlive(int pid)
{
try
{
using var process = Process.GetProcessById(pid);
return !process.HasExited;
}
catch (ArgumentException)
{
return false;
}
}
}