Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft. Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch. WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b umbenannten Parameter angeglichen. Dabei behoben: - 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue" gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day. - Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine CSS-Klasse gab - jetzt "success" wie laufende Instanzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
7.1 KiB
C#
190 lines
7.1 KiB
C#
namespace ClawdDotNet.Manager.Tests;
|
|
|
|
/// <summary>
|
|
/// An instance process the test controls.
|
|
///
|
|
/// <para>A real process would only allow checking supervision superficially: a crash at the
|
|
/// desired moment, a process that lives and stays silent, a reused
|
|
/// process ID — all of that can either not be produced with a running program or only
|
|
/// with waiting times, and tests with waiting times are tests that
|
|
/// eventually get switched off.</para>
|
|
/// </summary>
|
|
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; }
|
|
|
|
/// <summary>The process ends — with or without intervention.</summary>
|
|
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; }
|
|
|
|
/// <summary>Which process belongs to which instance directory.</summary>
|
|
public Dictionary<string, FakeProcess> ByDirectory { get; } =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>Whether a process is currently alive for this directory.</summary>
|
|
public bool IsRunning(string instanceDirectory)
|
|
=> ByDirectory.TryGetValue(Path.GetFullPath(instanceDirectory), out var p)
|
|
&& !p.HasExited;
|
|
|
|
/// <summary>If set, the start fails with this.</summary>
|
|
public Exception? FailStartWith { get; set; }
|
|
|
|
/// <summary>What <see cref="Attach"/> should deliver — already running processes.</summary>
|
|
public Dictionary<int, FakeProcess> 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;
|
|
ByDirectory[Path.GetFullPath(instanceDirectory)] = Last;
|
|
return Last;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A process that was already running before this manager came (E-M6).
|
|
///
|
|
/// <para>Does not count as a start — exactly therein lies the difference several tests
|
|
/// check: what runs is taken over and not booted a second time.</para>
|
|
/// </summary>
|
|
public FakeProcess Fremdstart(string instanceDirectory, int processId)
|
|
{
|
|
var prozess = new FakeProcess(processId, Now);
|
|
Running[processId] = prozess;
|
|
ByDirectory[Path.GetFullPath(instanceDirectory)] = prozess;
|
|
return prozess;
|
|
}
|
|
|
|
public IInstanceProcess? Attach(int processId, DateTimeOffset startedAt)
|
|
{
|
|
if (!Running.TryGetValue(processId, out var process))
|
|
return null;
|
|
|
|
// The same check as in real operation: same ID, but a different start time
|
|
// means a different process.
|
|
return (process.StartedAt - startedAt).Duration() > TimeSpan.FromSeconds(2)
|
|
? null
|
|
: process;
|
|
}
|
|
}
|
|
|
|
/// <summary>The socket that the test lets answer or stay silent.</summary>
|
|
internal sealed class FakeChannel : IInstanceChannel
|
|
{
|
|
/// <summary>The answer for all directories, unless one of their own is stored.</summary>
|
|
public InstanceIdentity? Answer { get; set; }
|
|
|
|
/// <summary>
|
|
/// Answers per instance directory.
|
|
///
|
|
/// <para>Needed as soon as more than one instance is involved: information that names the same
|
|
/// instance for every directory lets tests turn green that in operation would mix up two
|
|
/// different instances.</para>
|
|
/// </summary>
|
|
public Dictionary<string, InstanceIdentity?> AnswerFor { get; } =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>
|
|
/// If set, the socket only answers for instances that this launcher
|
|
/// actually started — as in operation.
|
|
///
|
|
/// <para><b>Without this a test setup is easily wrong:</b> whoever stores the answer
|
|
/// before the manager comes up says with it "this instance is already running". The manager
|
|
/// then takes it over (E-M6), the launcher is never touched — and the test checks something
|
|
/// other than it claims. That happened to me three times; with this coupling it can
|
|
/// no longer happen.</para>
|
|
///
|
|
/// <para>Not set means: the stored answer always applies. That is needed by whoever wants to describe an
|
|
/// instance that runs without this manager having started it.</para>
|
|
/// </summary>
|
|
public FakeLauncher? Launcher { get; set; }
|
|
|
|
/// <summary>
|
|
/// Directories whose instance has fallen silent: the process lives, the socket is silent.
|
|
///
|
|
/// <para>The case from #529 — and the only one in which supervision resorts to
|
|
/// force on its own initiative. Without this switch it cannot be reproduced: the coupling
|
|
/// to <see cref="Launcher"/> would otherwise bind the answer to exactly what is supposed to
|
|
/// fall apart here.</para>
|
|
/// </summary>
|
|
public HashSet<string> Verstummt { get; } = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>The instance stops answering without its process ending.</summary>
|
|
public void Verstummen(string instanceDirectory)
|
|
=> Verstummt.Add(Path.GetFullPath(instanceDirectory));
|
|
|
|
public int Calls { get; private set; }
|
|
|
|
/// <summary>How often the instance was asked to end itself.</summary>
|
|
public int StopRequests { get; private set; }
|
|
|
|
/// <summary>Whether the request arrives. False means: the instance no longer listens at all.</summary>
|
|
public bool StopRequestArrives { get; set; } = true;
|
|
|
|
public Task<InstanceIdentity?> AskAsync(string instanceDirectory, CancellationToken ct = default)
|
|
{
|
|
Calls++;
|
|
|
|
if (Verstummt.Contains(Path.GetFullPath(instanceDirectory)))
|
|
return Task.FromResult<InstanceIdentity?>(null);
|
|
|
|
if (Launcher is not null && !Launcher.IsRunning(instanceDirectory))
|
|
return Task.FromResult<InstanceIdentity?>(null);
|
|
|
|
return Task.FromResult(
|
|
AnswerFor.TryGetValue(Path.GetFullPath(instanceDirectory), out var eigene)
|
|
? eigene
|
|
: Answer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// #722: What the instance does when the request arrives — typically: end. A test that
|
|
/// ended the process before asking raced the supervisor's own check: under load the check
|
|
/// saw the process gone first and called it a crash, not a stop.
|
|
/// </summary>
|
|
public Action<string>? OnStopRequest { get; set; }
|
|
|
|
public Task<bool> RequestStopAsync(string instanceDirectory, CancellationToken ct = default)
|
|
{
|
|
StopRequests++;
|
|
if (StopRequestArrives)
|
|
OnStopRequest?.Invoke(instanceDirectory);
|
|
return Task.FromResult(StopRequestArrives);
|
|
}
|
|
}
|