From ac6e6e11e6d1f814f1f649caace5c2b18f9c3283 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 28 Sep 2026 20:49:07 +0200 Subject: [PATCH] fix: fence the base-map cache test on a re-fetch, not a post count A_server_that_is_no_longer_connected_loses_its_cached_base_map failed intermittently on the Windows runner with "Expected: 2, Actual: 1". Priming republishes a marker event every 20 ms until the first post lands, which leaves dozens of repaints queued behind it. Those keep posting from the tile they already hold, so "post count reaches 2" could be satisfied by a repaint composed before the disconnect. When the test thread was starved long enough for that queue to drain, nothing was ever repainted after the eviction and the fetch count stayed at 1. Wait for the second base-map fetch instead: only a repaint that found the cache empty can produce it. The assertion is now "at least two", because the disconnect is republished as well and every copy evicts the tile again. Co-Authored-By: Claude Fable 5.1 --- .../Hosting/MapRefreshTests.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/RustPlusBot.Features.Map.Tests/Hosting/MapRefreshTests.cs b/tests/RustPlusBot.Features.Map.Tests/Hosting/MapRefreshTests.cs index 0ba1011..8cf4841 100644 --- a/tests/RustPlusBot.Features.Map.Tests/Hosting/MapRefreshTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/Hosting/MapRefreshTests.cs @@ -148,15 +148,20 @@ public async Task A_server_that_is_no_longer_connected_loses_its_cached_base_map await h.PublishUntilAsync(() => new ConnectionStatusChangedEvent(Guild, Server, false, true), h.WhenStatusHandled); + // Priming republishes until the first post lands, so repaints queued before the disconnect are + // still being drained and keep posting. A post count cannot tell those from a repaint made + // after the disconnect; a second fetch can only come from one that found the cache empty. await h.PublishUntilAsync(() => new MapMarkersChangedEvent(Guild, Server, null, [], [], []), - h.PostCountReachesAsync(2)); + h.BaseMapFetchesReachAsync(2)); } finally { await h.Service.StopAsync(CancellationToken.None); } - Assert.Equal(2, h.BaseMapFetches); + // Not exactly two: the disconnect is republished too, and each copy evicts the tile again. + Assert.True(h.BaseMapFetches >= 2, + $"the cached base map survived the disconnect (fetches: {h.BaseMapFetches})"); } [Fact] @@ -392,6 +397,8 @@ private sealed class FakeBaseMapSource(byte[]? jpeg, Action onFetch) : IBaseMapS private sealed class Harness : IDisposable { + private readonly List<(int Count, TaskCompletionSource Tcs)> _baseMapFetchTargets = []; + private readonly ConcurrentDictionary> _channelPostTargets = new(); @@ -498,6 +505,12 @@ public static Harness Create( public Task PostCountReachesAsync(int count) => WaitForCountAsync(_postTargets, count, Posts); + /// Completes once the base map has been fetched times. + /// The fetch count to wait for. + /// A task that completes when the base map has been fetched that many times. + public Task BaseMapFetchesReachAsync(int count) => + WaitForCountAsync(_baseMapFetchTargets, count, BaseMapFetches); + /// How many posts one channel has received so far. /// The channel to count. /// The post count for that channel. @@ -565,7 +578,8 @@ private static IRigState Rigs() return rigs; } - private void OnBaseMapFetch() => Interlocked.Increment(ref _baseMapFetches); + private void OnBaseMapFetch() => + ReleaseReached(_baseMapFetchTargets, Interlocked.Increment(ref _baseMapFetches)); private Task OnPostAsync(ulong channelId) {