Files
nucleic-windows/NucleicBroker.Tests/FakeWslc.cs
T
2026-07-27 22:02:34 -07:00

190 lines
6.2 KiB
C#

using NucleicBroker;
namespace NucleicBroker.Tests;
/// <summary>
/// In-memory IWslc for the broker contract tests (docs/WINDOWS_PORT.md §14): records calls,
/// serves canned state, and exposes the event sink so tests can emit process output/exit and
/// session-death exactly as the wslc facade would — from arbitrary threads, asynchronously.
/// </summary>
public sealed class FakeWslc : IWslc
{
public IBrokerEvents? Events;
public List<string> Calls = [];
public Dictionary<string, string> ContainerStates = [];
public List<ImageInfo> Images = [];
public List<string> MissingComponents = [];
public string Gateway = "172.28.0.1";
public SessionSpec? LastSession;
public ContainerCreateSpec? LastCreate;
public (string Name, int Signal, int GraceMs)? LastStop;
public FakeProcess? LastProcess;
public long LastProcId;
public Exception? NextError;
public string? WslcVersion => "9.9-test";
public void SetEvents(IBrokerEvents events) => Events = events;
private void Record(string call)
{
Calls.Add(call);
if (NextError is { } error)
{
NextError = null;
throw error;
}
}
public Task<IReadOnlyList<string>> MissingComponentsAsync(CancellationToken ct)
{
Record("components.missing");
return Task.FromResult<IReadOnlyList<string>>(MissingComponents);
}
public Task InstallComponentsAsync(CancellationToken ct)
{
Record("components.install");
Events?.InstallProgress("installing", 50);
Events?.InstallProgress("done", 100);
return Task.CompletedTask;
}
public Task<string> EnsureSessionAsync(SessionSpec spec, CancellationToken ct)
{
Record("session.ensure");
LastSession = spec;
return Task.FromResult(Gateway);
}
public Task TerminateSessionAsync(CancellationToken ct)
{
Record("session.terminate");
return Task.CompletedTask;
}
public Task PullImageAsync(string reference, RegistryAuth? auth, CancellationToken ct)
{
Record($"image.pull:{reference}");
Events?.PullProgress(reference, "downloading", 10, 100);
Events?.PullProgress(reference, "done", 100, 100);
Images.Add(new ImageInfo(reference, "sha256:fake", 1234));
return Task.CompletedTask;
}
public Task<IReadOnlyList<ImageInfo>> ListImagesAsync(CancellationToken ct)
{
Record("image.list");
return Task.FromResult<IReadOnlyList<ImageInfo>>(Images);
}
public Task DeleteImageAsync(string reference, CancellationToken ct)
{
Record($"image.delete:{reference}");
Images.RemoveAll(i => i.Ref == reference);
return Task.CompletedTask;
}
public Task<ImageInfo?> InspectImageAsync(string reference, CancellationToken ct)
{
Record($"image.inspect:{reference}");
return Task.FromResult(Images.FirstOrDefault(i => i.Ref == reference));
}
public Task CreateContainerAsync(ContainerCreateSpec spec, CancellationToken ct)
{
Record($"container.create:{spec.Name}");
LastCreate = spec;
ContainerStates[spec.Name] = "stopped";
return Task.CompletedTask;
}
public Task StartContainerAsync(string name, CancellationToken ct)
{
Record($"container.start:{name}");
if (!ContainerStates.ContainsKey(name))
throw new WslcError(WslcError.NotFound, $"no container named {name}");
ContainerStates[name] = "running";
return Task.CompletedTask;
}
public Task StopContainerAsync(string name, int signal, int graceMs, CancellationToken ct)
{
Record($"container.stop:{name}");
LastStop = (name, signal, graceMs);
ContainerStates[name] = "stopped";
return Task.CompletedTask;
}
public Task DeleteContainerAsync(string name, bool force, CancellationToken ct)
{
Record($"container.delete:{name}");
ContainerStates.Remove(name);
return Task.CompletedTask;
}
public Task<IReadOnlyList<ContainerInfo>> ListContainersAsync(CancellationToken ct)
{
Record("container.list");
return Task.FromResult<IReadOnlyList<ContainerInfo>>(
ContainerStates.Select(kv => new ContainerInfo(kv.Key, "img", kv.Value)).ToList());
}
public Task<string> ContainerStateAsync(string name, CancellationToken ct)
{
Record($"container.state:{name}");
return Task.FromResult(ContainerStates.GetValueOrDefault(name, "absent"));
}
public Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct)
{
Record($"container.stats:{name}");
return Task.FromResult<ContainerStatsInfo?>(
ContainerStates.GetValueOrDefault(name) == "running"
? new ContainerStatsInfo(111, 222, 333, 0)
: null);
}
public Task<IWslcProcess> ExecAsync(long procId, ProcSpec spec, CancellationToken ct)
{
Record($"proc.exec:{spec.Container}:{string.Join(' ', spec.Argv)}");
if (ContainerStates.GetValueOrDefault(spec.Container) != "running")
throw new WslcError(WslcError.NotRunning, $"container {spec.Container} isn't running");
LastProcId = procId;
LastProcess = new FakeProcess();
return Task.FromResult<IWslcProcess>(LastProcess);
}
public sealed class FakeProcess : IWslcProcess
{
public MemoryStream Stdin = new();
public bool StdinClosed;
public List<int> Signals = [];
public (int Cols, int Rows)? LastResize;
public Task WriteStdinAsync(ReadOnlyMemory<byte> data, CancellationToken ct)
{
Stdin.Write(data.Span);
return Task.CompletedTask;
}
public Task CloseStdinAsync(CancellationToken ct)
{
StdinClosed = true;
return Task.CompletedTask;
}
public Task SignalAsync(int signal, CancellationToken ct)
{
Signals.Add(signal);
return Task.CompletedTask;
}
public Task ResizeAsync(int cols, int rows, CancellationToken ct)
{
LastResize = (cols, rows);
return Task.CompletedTask;
}
}
}