2026-07-27 22:02:34 -07:00
|
|
|
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;
|
|
|
|
|
|
2026-07-29 17:09:35 -07:00
|
|
|
/// <summary>Set before a proc.exec to make that process emit during StartAsync — i.e. behave
|
|
|
|
|
/// like a command that finishes instantly.</summary>
|
|
|
|
|
public Action? NextProcessOnStart;
|
|
|
|
|
|
2026-07-27 22:02:34 -07:00
|
|
|
public string? WslcVersion => "9.9-test";
|
|
|
|
|
|
2026-07-29 02:52:41 -07:00
|
|
|
/// The fake is deliberately fully-capable: it stands in for a facade with BOTH wslc surfaces
|
|
|
|
|
/// bound, so the broker's happy path is what these tests exercise. Degradation is the
|
|
|
|
|
/// facade's business (docs/WINDOWS_PORT.md D13), and tests that care set this directly.
|
|
|
|
|
public List<string> CapabilityList = ["enumerate", "reattach", "tty", "stats"];
|
|
|
|
|
public IReadOnlyList<string> Capabilities => CapabilityList;
|
|
|
|
|
|
2026-07-27 22:02:34 -07:00
|
|
|
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;
|
2026-07-29 17:09:35 -07:00
|
|
|
// Mirror the real facade: create and attach, but do NOT run — BrokerService runs it after
|
|
|
|
|
// the procId response is on the wire.
|
|
|
|
|
LastProcess = new FakeProcess { OnStart = NextProcessOnStart };
|
|
|
|
|
NextProcessOnStart = null;
|
2026-07-27 22:02:34 -07:00
|
|
|
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;
|
2026-07-29 17:09:35 -07:00
|
|
|
public bool Started;
|
|
|
|
|
|
|
|
|
|
/// <summary>Lets a test stand in for a process that finishes DURING start — an `echo`,
|
|
|
|
|
/// which is what exposed the response-ordering bug on hardware.</summary>
|
|
|
|
|
public Action? OnStart;
|
|
|
|
|
|
|
|
|
|
public Task StartAsync(CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
Started = true;
|
|
|
|
|
OnStart?.Invoke();
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
2026-07-27 22:02:34 -07:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|