namespace ClawdDotNet.Manager.Tests; /// /// Ein Instanzprozess, den der Test steuert. /// /// Ein echter Prozess ließe die Aufsicht nur oberflächlich prüfen: Ein Absturz zum /// gewünschten Zeitpunkt, ein Prozess, der lebt und schweigt, eine wiederverwendete /// Prozesskennung — das alles ist mit einem laufenden Programm entweder nicht oder nur /// mit Wartezeiten herzustellen, und Tests mit Wartezeiten sind Tests, die man /// irgendwann abschaltet. /// internal sealed class FakeProcess(int id, DateTimeOffset startedAt) : IInstanceProcess { public int Id { get; } = id; public DateTimeOffset StartedAt { get; } = startedAt; public bool HasExited { get; private set; } public int? ExitCode { get; private set; } public bool WasKilled { get; private set; } public bool Disposed { get; private set; } /// Der Prozess endet — mit oder ohne Zutun. public void Exit(int code) { HasExited = true; ExitCode = code; } public void Kill() { WasKilled = true; if (!HasExited) Exit(-1); } public void Dispose() => Disposed = true; } internal sealed class FakeLauncher : IInstanceLauncher { private int _nextId = 1000; public FakeProcess? Last { get; private set; } public int Starts { get; private set; } /// Wenn gesetzt, scheitert der Start damit. public Exception? FailStartWith { get; set; } /// Was liefern soll — bereits laufende Prozesse. public Dictionary Running { get; } = []; public DateTimeOffset Now { get; set; } = DateTimeOffset.UnixEpoch; public IInstanceProcess Start(string instanceDirectory) { Starts++; if (FailStartWith is not null) throw FailStartWith; Last = new FakeProcess(_nextId++, Now); Running[Last.Id] = Last; return Last; } public IInstanceProcess? Attach(int processId, DateTimeOffset startedAt) { if (!Running.TryGetValue(processId, out var process)) return null; // Dieselbe Pruefung wie im Echtbetrieb: gleiche Kennung, aber andere Startzeit // heisst anderer Prozess. return (process.StartedAt - startedAt).Duration() > TimeSpan.FromSeconds(2) ? null : process; } } /// Der Socket, den der Test antworten oder schweigen lässt. internal sealed class FakeChannel : IInstanceChannel { /// Die Antwort für alle Verzeichnisse, sofern keine eigene hinterlegt ist. public InstanceIdentity? Answer { get; set; } /// /// Antworten je Instanzverzeichnis. /// /// Nötig, sobald mehr als eine Instanz im Spiel ist: Eine Auskunft, die für jedes /// Verzeichnis dieselbe Instanz nennt, lässt Tests grün werden, die im Betrieb zwei /// verschiedene Instanzen verwechseln würden. /// public Dictionary AnswerFor { get; } = new(StringComparer.OrdinalIgnoreCase); public int Calls { get; private set; } /// Wie oft die Instanz gebeten wurde, sich zu beenden. public int StopRequests { get; private set; } /// Ob die Bitte ankommt. Falsch heisst: Die Instanz hoert gar nicht mehr zu. public bool StopRequestArrives { get; set; } = true; public Task AskAsync(string instanceDirectory, CancellationToken ct = default) { Calls++; return Task.FromResult( AnswerFor.TryGetValue(Path.GetFullPath(instanceDirectory), out var eigene) ? eigene : Answer); } public Task RequestStopAsync(string instanceDirectory, CancellationToken ct = default) { StopRequests++; return Task.FromResult(StopRequestArrives); } }