98 lines
3 KiB
C#
98 lines
3 KiB
C#
using System.Diagnostics;
|
|
using w4c_workflows.Services.Execution;
|
|
using Xunit;
|
|
|
|
namespace w4c_workflows.Tests;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<TimeoutException>(() => 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<OperationCanceledException>(() => 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;
|
|
}
|
|
}
|
|
}
|