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; } } }