From 753d633cf35257eccf511541ce82ebf4f8062dff Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 2 Oct 2026 11:50:51 +0200 Subject: [PATCH] Linux-Wackeltests: Instanz endet erst auf die Stopp-Bitte, Lesetest schreibt bis genug gelesen ist (#722) Drei Manager-Tests liessen den Prozess vor der Stopp-Bitte enden; unter Last sah der Aufsichtstakt das zuerst und meldete Crashed statt Stopped. Die FakeChannel bekommt OnStopRequest, die Tests beenden darin. Der Drain-Test bekommt 20 s statt 2 s Luft. AtomicFile schreibt, bis der Leser 20 Blicke hatte, statt fest 40 Mal. Co-Authored-By: Claude Opus 5.5 --- .../Storage/AtomicFileTests.cs | 12 +++++++----- tests/ClawdDotNet.Manager.Tests/Doubles.cs | 9 +++++++++ .../InstanceRegistryTests.cs | 8 ++++---- .../InstanceSupervisorTests.cs | 12 +++++++----- tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs | 4 ++-- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/tests/ClawdDotNet.Core.Tests/Storage/AtomicFileTests.cs b/tests/ClawdDotNet.Core.Tests/Storage/AtomicFileTests.cs index 349cbe4..8a5fc7d 100644 --- a/tests/ClawdDotNet.Core.Tests/Storage/AtomicFileTests.cs +++ b/tests/ClawdDotNet.Core.Tests/Storage/AtomicFileTests.cs @@ -119,6 +119,9 @@ public sealed class AtomicFileTests : IDisposable AtomicFile.WriteAllText(path, klein); + var fehler = 0; + var gelesen = 0; + // Harte Zeitgrenze, damit ein Fehler im Schreiber den Test nicht hängen lässt. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); @@ -126,7 +129,9 @@ public sealed class AtomicFileTests : IDisposable { try { - for (var i = 0; i < 40; i++) + // #722: Geschrieben wird, bis der Leser genug gesehen hat — nicht eine feste + // Zahl von Malen. Unter Parallellast kam der Leser sonst nur auf fuenf Blicke. + for (var i = 0; (i < 40 || Volatile.Read(ref gelesen) < 20) && !cts.IsCancellationRequested; i++) { AtomicFile.WriteAllText(path, i % 2 == 0 ? gross : klein); await Task.Delay(1); @@ -138,9 +143,6 @@ public sealed class AtomicFileTests : IDisposable } }); - var fehler = 0; - var gelesen = 0; - var leser = Task.Run(async () => { while (!cts.IsCancellationRequested) @@ -150,7 +152,7 @@ public sealed class AtomicFileTests : IDisposable // AtomicFile.ReadAllText, damit der Leser den Schreiber nicht // blockiert — genau dafür gibt es den Helfer. var inhalt = AtomicFile.ReadAllText(path); - gelesen++; + Interlocked.Increment(ref gelesen); // Muss immer ein vollständiges JSON-Array sein. JsonSerializer.Deserialize(inhalt); diff --git a/tests/ClawdDotNet.Manager.Tests/Doubles.cs b/tests/ClawdDotNet.Manager.Tests/Doubles.cs index f514947..4880cf8 100644 --- a/tests/ClawdDotNet.Manager.Tests/Doubles.cs +++ b/tests/ClawdDotNet.Manager.Tests/Doubles.cs @@ -172,9 +172,18 @@ internal sealed class FakeChannel : IInstanceChannel : Answer); } + /// + /// #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. + /// + public Action? OnStopRequest { get; set; } + public Task RequestStopAsync(string instanceDirectory, CancellationToken ct = default) { StopRequests++; + if (StopRequestArrives) + OnStopRequest?.Invoke(instanceDirectory); return Task.FromResult(StopRequestArrives); } diff --git a/tests/ClawdDotNet.Manager.Tests/InstanceRegistryTests.cs b/tests/ClawdDotNet.Manager.Tests/InstanceRegistryTests.cs index 076ff3b..b550b79 100644 --- a/tests/ClawdDotNet.Manager.Tests/InstanceRegistryTests.cs +++ b/tests/ClawdDotNet.Manager.Tests/InstanceRegistryTests.cs @@ -139,8 +139,8 @@ public sealed class InstanceRegistryTests : IAsyncDisposable register.Instances.Single().Condition.ShouldBe(InstanceCondition.Running); // Die Instanz endet auf die Bitte hin. - _launcher.Last!.Exit(0); - _kanal.Answer = null; + var prozess = _launcher.Last!; + _kanal.OnStopRequest = _ => { _kanal.Answer = null; prozess.Exit(0); }; (await register.StopAsync(pfad)).ShouldBeTrue(); register.Instances.Single().Condition.ShouldBe(InstanceCondition.Stopped); @@ -230,8 +230,8 @@ public sealed class InstanceRegistryTests : IAsyncDisposable var a = register.Create("Alpha"); await register.StartAsync(a); - _launcher.Last!.Exit(0); - _kanal.Answer = null; + var prozess = _launcher.Last!; + _kanal.OnStopRequest = _ => { _kanal.Answer = null; prozess.Exit(0); }; await register.StopAllAsync("Lizenz widerrufen"); diff --git a/tests/ClawdDotNet.Manager.Tests/InstanceSupervisorTests.cs b/tests/ClawdDotNet.Manager.Tests/InstanceSupervisorTests.cs index c2dca95..04e9f12 100644 --- a/tests/ClawdDotNet.Manager.Tests/InstanceSupervisorTests.cs +++ b/tests/ClawdDotNet.Manager.Tests/InstanceSupervisorTests.cs @@ -59,10 +59,10 @@ public sealed class InstanceSupervisorTests : IDisposable catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ } } - private InstanceSupervisor Aufsicht() + private InstanceSupervisor Aufsicht(SupervisionOptions? options = null) { var supervisor = new InstanceSupervisor( - _dir, _launcher, _kanal, NullLogger.Instance, Options); + _dir, _launcher, _kanal, NullLogger.Instance, options ?? Options); supervisor.Observed += _ereignisse.Add; return supervisor; @@ -307,16 +307,18 @@ public sealed class InstanceSupervisorTests : IDisposable { _kanal.Answer = Test; - await using var aufsicht = Aufsicht(); + // #722: Viel Luft bis MaxDrainWait — unter Parallellast kam die Verzoegerung von 700 ms + // sonst erst nach den 2 s an, und der Test sah einen Abschuss, wo keiner hingehoert. + await using var aufsicht = Aufsicht(Options with { MaxDrainWait = TimeSpan.FromSeconds(20) }); await aufsicht.StartAsync(); var prozess = _launcher.Last.ShouldNotBeNull(); // Sie antwortet und meldet: Ich raeume auf. Laenger als StopGrace (300ms), aber - // kuerzer als MaxDrainWait (2s). + // kuerzer als MaxDrainWait. _kanal.Answer = Test with { Phase = ShutdownPhase.Draining, OpenWork = 1 }; - _ = Task.Run(async () => + _kanal.OnStopRequest = _ => Task.Run(async () => { await Task.Delay(700); _kanal.Answer = null; diff --git a/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs b/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs index ad22252..24c3356 100644 --- a/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs +++ b/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs @@ -182,8 +182,8 @@ public sealed class ManagerCoreTests : IAsyncDisposable manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Running); // Die Instanz endet auf die Bitte hin. - _launcher.Last!.Exit(0); - _kanal.AnswerFor[Path.GetFullPath(pfad)] = null; + var prozess = _launcher.Last!; + _kanal.OnStopRequest = _ => { _kanal.AnswerFor[Path.GetFullPath(pfad)] = null; prozess.Exit(0); }; await manager.HandleLicenseRevokedAsync("Widerrufen durch den Hersteller");