w4c-workflows-api/w4c-workflows-api.Tests/SubprocessScriptExecutorTests.cs

152 lines
5.1 KiB
C#
Raw Normal View History

using System.Text.Json;
using Microsoft.Extensions.Configuration;
using w4c_workflows.Services.Execution;
using Xunit;
namespace w4c_workflows.Tests;
public class SubprocessScriptExecutorTests
{
private static SubprocessScriptExecutor Shell(int timeoutSeconds = 60)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Workflows:TaskTimeoutSeconds"] = timeoutSeconds.ToString(),
})
.Build();
return new SubprocessScriptExecutor("shell", "sh", config);
}
[Fact]
public async Task Shell_reads_stdin_and_returns_json_output()
{
using var dir = new TempDir();
dir.Write("task.sh", """
#!/bin/sh
read input
echo "{\"ok\":true,\"value\":$input}"
""");
var result = await Shell().ExecuteAsync(
Invocation.For("shell", "task.sh", "{\"x\":1}", dir.Path), default);
Assert.True(result.Success, result.Error);
Assert.Equal(0, result.ExitCode);
using var output = JsonDocument.Parse(result.Output!);
Assert.True(output.RootElement.GetProperty("ok").GetBoolean());
}
[Fact]
public async Task Shell_nonzero_exit_reports_stderr_as_error()
{
using var dir = new TempDir();
dir.Write("task.sh", """
#!/bin/sh
echo "boom" 1>&2
exit 3
""");
var result = await Shell().ExecuteAsync(
Invocation.For("shell", "task.sh", "{}", dir.Path), default);
Assert.False(result.Success);
Assert.Equal(3, result.ExitCode);
Assert.Contains("boom", result.Error);
}
[Fact]
public async Task Shell_non_json_stdout_is_wrapped_as_string()
{
using var dir = new TempDir();
dir.Write("task.sh", "#!/bin/sh\necho \"hello world\"\n");
var result = await Shell().ExecuteAsync(
Invocation.For("shell", "task.sh", "{}", dir.Path), default);
Assert.True(result.Success, result.Error);
using var output = JsonDocument.Parse(result.Output!);
Assert.Equal("hello world", output.RootElement.GetString());
}
[Fact]
public async Task Shell_is_killed_when_it_exceeds_the_time_limit()
{
using var dir = new TempDir();
dir.Write("task.sh", "#!/bin/sh\nsleep 5\n");
var result = await Shell(timeoutSeconds: 1).ExecuteAsync(
Invocation.For("shell", "task.sh", "{}", dir.Path), default);
Assert.False(result.Success);
Assert.Contains("time limit", result.Error);
}
2026-09-11 12:06:06 +00:00
[Fact]
public async Task Shell_honours_bash_shebang_for_bash_only_scripts()
{
if (ProcessRunner.FindInPath("bash") is null)
return; // no bash on this host — the fallback path is covered elsewhere
using var dir = new TempDir();
dir.Write("task.sh", """
#!/usr/bin/env bash
set -euo pipefail
cat >/dev/null || true
echo '{"ok":true}'
""");
var result = await Shell().ExecuteAsync(
Invocation.For("shell", "task.sh", "{}", dir.Path), default);
Assert.True(result.Success, result.Error);
using var output = JsonDocument.Parse(result.Output!);
Assert.True(output.RootElement.GetProperty("ok").GetBoolean());
}
[Fact]
public async Task Python_reads_stdin_and_returns_json_output()
{
var executor = new SubprocessScriptExecutor("python", "python3", new ConfigurationBuilder().Build());
if (!executor.IsAvailable())
return; // python3 not installed on this host — availability is covered by RuntimeRegistryTests
using var dir = new TempDir();
dir.Write("task.py", """
import sys, json
print(json.dumps({"echo": sys.stdin.read().strip()}))
""");
var result = await executor.ExecuteAsync(
Invocation.For("python", "task.py", "{\"x\":1}", dir.Path), default);
Assert.True(result.Success, result.Error);
Assert.Contains("\\\"x\\\":1", result.Output);
}
[Fact]
public async Task Javascript_runs_under_node()
{
var executor = new SubprocessScriptExecutor("javascript", "node", new ConfigurationBuilder().Build());
if (!executor.IsAvailable())
return; // node not installed on this host — availability is covered by RuntimeRegistryTests
using var dir = new TempDir();
dir.Write("task.js", """
let input = "";
process.stdin.on("data", (c) => (input += c));
process.stdin.on("end", () => {
const data = JSON.parse(input);
console.log(JSON.stringify({ doubled: data.n * 2 }));
});
""");
var result = await executor.ExecuteAsync(
Invocation.For("javascript", "task.js", "{\"n\":21}", dir.Path), default);
Assert.True(result.Success, result.Error);
using var output = JsonDocument.Parse(result.Output!);
Assert.Equal(42, output.RootElement.GetProperty("doubled").GetInt32());
}
}