diff --git a/Directory.Packages.props b/Directory.Packages.props index def38fa7..99f3a5b2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -46,6 +46,6 @@ - + diff --git a/src/RustPlusBot.Features.Alarms/Hosting/AlarmsHostedService.cs b/src/RustPlusBot.Features.Alarms/Hosting/AlarmsHostedService.cs index 54d822d1..de148d91 100644 --- a/src/RustPlusBot.Features.Alarms/Hosting/AlarmsHostedService.cs +++ b/src/RustPlusBot.Features.Alarms/Hosting/AlarmsHostedService.cs @@ -49,12 +49,12 @@ public async Task StopAsync(CancellationToken cancellationToken) foreach (var loop in new[] { _pairedLoop, _triggeredLoop, _statusLoop, _reachabilityLoop, _observedLoop, _wipedLoop - }.Where(t => t is not null)) + }.OfType()) { try { #pragma warning disable VSTHRD003 // Our own loop tasks, joined on stop. - await loop!.ConfigureAwait(false); + await loop.ConfigureAwait(false); #pragma warning restore VSTHRD003 } catch (OperationCanceledException) diff --git a/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs b/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs index 451c2311..5599a905 100644 --- a/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs +++ b/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs @@ -173,7 +173,9 @@ public async Task EnsureConnectionAsync(ulong guildId, Guid serverId, Cancellati /// public async Task StopAsync(ulong guildId, Guid serverId) { - await _gate.WaitAsync().ConfigureAwait(false); + // CancellationToken.None, not _shutdown.Token: teardown must still acquire the gate after + // StopAllAsync has already cancelled _shutdown, otherwise the connection is never stopped. + await _gate.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { await StopConnectionAsync((guildId, serverId)).ConfigureAwait(false); @@ -188,7 +190,8 @@ public async Task StopAsync(ulong guildId, Guid serverId) public async Task StopAllAsync() { await _shutdown.CancelAsync().ConfigureAwait(false); - await _gate.WaitAsync().ConfigureAwait(false); + // CancellationToken.None: _shutdown was just cancelled, so waiting on it would abandon shutdown. + await _gate.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { foreach (var key in _connections.Keys.ToList()) diff --git a/src/RustPlusBot.Features.Events/Hosting/EventsHostedService.cs b/src/RustPlusBot.Features.Events/Hosting/EventsHostedService.cs index 22aa08b8..f2a892e2 100644 --- a/src/RustPlusBot.Features.Events/Hosting/EventsHostedService.cs +++ b/src/RustPlusBot.Features.Events/Hosting/EventsHostedService.cs @@ -60,12 +60,12 @@ public async Task StopAsync(CancellationToken cancellationToken) foreach (var loop in new[] { _relayLoop, _rigLoop, _tickLoop, _disconnectLoop - }.Where(t => t is not null)) + }.OfType()) { try { #pragma warning disable VSTHRD003 // Our own loop tasks, joined on stop. - await loop!.ConfigureAwait(false); + await loop.ConfigureAwait(false); #pragma warning restore VSTHRD003 } catch (OperationCanceledException) diff --git a/src/RustPlusBot.Features.Map/Hosting/InfoMapHostedService.cs b/src/RustPlusBot.Features.Map/Hosting/InfoMapHostedService.cs index ac3fb976..0e6401c0 100644 --- a/src/RustPlusBot.Features.Map/Hosting/InfoMapHostedService.cs +++ b/src/RustPlusBot.Features.Map/Hosting/InfoMapHostedService.cs @@ -58,12 +58,12 @@ public async Task StopAsync(CancellationToken cancellationToken) foreach (var loop in new[] { _statusLoop, _tickLoop - }.Where(t => t is not null)) + }.OfType()) { try { #pragma warning disable VSTHRD003 // Our own loop tasks, joined on stop. - await loop!.ConfigureAwait(false); + await loop.ConfigureAwait(false); #pragma warning restore VSTHRD003 } catch (OperationCanceledException) diff --git a/src/RustPlusBot.Features.Map/Hosting/MapHostedService.cs b/src/RustPlusBot.Features.Map/Hosting/MapHostedService.cs index 9a113c71..4e7f556b 100644 --- a/src/RustPlusBot.Features.Map/Hosting/MapHostedService.cs +++ b/src/RustPlusBot.Features.Map/Hosting/MapHostedService.cs @@ -79,12 +79,12 @@ public async Task StopAsync(CancellationToken cancellationToken) foreach (var loop in new[] { _markerLoop, _settingsLoop, _statusLoop, _tickLoop - }.Where(t => t is not null)) + }.OfType()) { try { #pragma warning disable VSTHRD003 // Our own loop tasks, joined on stop. - await loop!.ConfigureAwait(false); + await loop.ConfigureAwait(false); #pragma warning restore VSTHRD003 } catch (OperationCanceledException) diff --git a/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs b/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs index 5b393275..8641e938 100644 --- a/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs +++ b/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs @@ -249,10 +249,11 @@ private async Task MarkExpiredAsync((ulong Guild, ulong Owner) key, Guid registr await using (expireScope.ConfigureAwait(false)) { var store = expireScope.ServiceProvider.GetRequiredService(); - await store.SetStatusAsync(registrationId, FcmRegistrationStatus.Expired).ConfigureAwait(false); + await store.SetStatusAsync(registrationId, FcmRegistrationStatus.Expired, _shutdown.Token) + .ConfigureAwait(false); } - await notifier.NotifyCredentialsExpiredAsync(key.Guild, key.Owner).ConfigureAwait(false); + await notifier.NotifyCredentialsExpiredAsync(key.Guild, key.Owner, _shutdown.Token).ConfigureAwait(false); } private sealed class Handle : IAsyncDisposable diff --git a/src/RustPlusBot.Features.StorageMonitors/Hosting/StorageMonitorsHostedService.cs b/src/RustPlusBot.Features.StorageMonitors/Hosting/StorageMonitorsHostedService.cs index 546349e8..5e6345fc 100644 --- a/src/RustPlusBot.Features.StorageMonitors/Hosting/StorageMonitorsHostedService.cs +++ b/src/RustPlusBot.Features.StorageMonitors/Hosting/StorageMonitorsHostedService.cs @@ -47,12 +47,12 @@ public async Task StopAsync(CancellationToken cancellationToken) foreach (var loop in new[] { _pairedLoop, _triggeredLoop, _statusLoop, _reachabilityLoop, _wipedLoop - }.Where(t => t is not null)) + }.OfType()) { try { #pragma warning disable VSTHRD003 // Our own loop tasks, joined on stop. - await loop!.ConfigureAwait(false); + await loop.ConfigureAwait(false); #pragma warning restore VSTHRD003 } catch (OperationCanceledException) diff --git a/src/RustPlusBot.Features.Switches/Hosting/SwitchesHostedService.cs b/src/RustPlusBot.Features.Switches/Hosting/SwitchesHostedService.cs index fd0537c4..61aae4cc 100644 --- a/src/RustPlusBot.Features.Switches/Hosting/SwitchesHostedService.cs +++ b/src/RustPlusBot.Features.Switches/Hosting/SwitchesHostedService.cs @@ -49,12 +49,12 @@ public async Task StopAsync(CancellationToken cancellationToken) foreach (var loop in new[] { _pairedLoop, _stateLoop, _statusLoop, _deviceLoop, _reachabilityLoop, _wipedLoop - }.Where(t => t is not null)) + }.OfType()) { try { #pragma warning disable VSTHRD003 // Our own loop tasks, joined on stop. - await loop!.ConfigureAwait(false); + await loop.ConfigureAwait(false); #pragma warning restore VSTHRD003 } catch (OperationCanceledException) diff --git a/src/RustPlusBot.Features.Wipes/Hosting/WipesHostedService.cs b/src/RustPlusBot.Features.Wipes/Hosting/WipesHostedService.cs index c5d217b9..ca9bfada 100644 --- a/src/RustPlusBot.Features.Wipes/Hosting/WipesHostedService.cs +++ b/src/RustPlusBot.Features.Wipes/Hosting/WipesHostedService.cs @@ -39,12 +39,12 @@ public async Task StopAsync(CancellationToken cancellationToken) foreach (var loop in new[] { _statusLoop, _wipedLoop - }.Where(t => t is not null)) + }.OfType()) { try { #pragma warning disable VSTHRD003 // Our own loop tasks, joined on stop. - await loop!.ConfigureAwait(false); + await loop.ConfigureAwait(false); #pragma warning restore VSTHRD003 } catch (OperationCanceledException) diff --git a/src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs b/src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs index 03ae039d..5ed88740 100644 --- a/src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs +++ b/src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs @@ -97,7 +97,7 @@ private Task OnReadyAsync() // Healing sweeps every provisioned guild's channels over REST; doing it inline blocks the // gateway task and stalls event dispatch, so offload it. Failures must be caught here — // nothing awaits this. - _ = Task.Run(HealProvisionedGuildsAsync); + _ = Task.Run(HealProvisionedGuildsAsync, _cts.Token); return Task.CompletedTask; } @@ -110,12 +110,16 @@ private async Task HealProvisionedGuildsAsync() { var store = scope.ServiceProvider.GetRequiredService(); var reconciler = scope.ServiceProvider.GetRequiredService(); - foreach (var guildId in await store.GetProvisionedGuildIdsAsync().ConfigureAwait(false)) + foreach (var guildId in await store.GetProvisionedGuildIdsAsync(_cts.Token).ConfigureAwait(false)) { - await reconciler.HealGuildAsync(guildId).ConfigureAwait(false); + await reconciler.HealGuildAsync(guildId, _cts.Token).ConfigureAwait(false); } } } + catch (OperationCanceledException) + { + // Shutting down. + } catch (Exception ex) // Broad catch is intentional: a faulting startup heal must not crash the host. { logger.LogError(ex, "Startup self-heal failed."); @@ -135,9 +139,13 @@ private async Task OnChannelDestroyedAsync(SocketChannel channel) await using (scope.ConfigureAwait(false)) { var reconciler = scope.ServiceProvider.GetRequiredService(); - await reconciler.HealGuildAsync(guildChannel.Guild.Id).ConfigureAwait(false); + await reconciler.HealGuildAsync(guildChannel.Guild.Id, _cts.Token).ConfigureAwait(false); } } + catch (OperationCanceledException) + { + // Shutting down. + } catch (Exception ex) // Broad catch is intentional: a faulting self-heal must not crash the host. { logger.LogError(ex, "Self-heal failed for guild {GuildId}.", guildChannel.Guild.Id); diff --git a/tests/RustPlusBot.Features.Connections.Tests/ClanSupervisorTests.cs b/tests/RustPlusBot.Features.Connections.Tests/ClanSupervisorTests.cs index 4b9b92a1..925ab2f1 100644 --- a/tests/RustPlusBot.Features.Connections.Tests/ClanSupervisorTests.cs +++ b/tests/RustPlusBot.Features.Connections.Tests/ClanSupervisorTests.cs @@ -177,9 +177,9 @@ public async Task Publishes_a_clan_state_event_on_connect() Assert.True(captured.TryPeek(out var evt)); Assert.NotNull(evt); - Assert.Equal(ClanProbeStatus.HasClan, evt!.Status); + Assert.Equal(ClanProbeStatus.HasClan, evt.Status); Assert.NotNull(evt.Snapshot); - Assert.Equal(1L, evt.Snapshot!.ClanId); + Assert.Equal(1L, evt.Snapshot.ClanId); await h.Supervisor.StopAllAsync(); await cts.CancelAsync(); @@ -220,7 +220,7 @@ public async Task Publishes_NoClan_on_connect_when_the_player_has_no_clan() Assert.True(captured.TryPeek(out var evt)); Assert.NotNull(evt); - Assert.Equal(ClanProbeStatus.NoClan, evt!.Status); + Assert.Equal(ClanProbeStatus.NoClan, evt.Status); Assert.Null(evt.Snapshot); await h.Supervisor.StopAllAsync(); @@ -264,7 +264,7 @@ public async Task Publishes_a_clan_state_event_when_the_socket_reports_a_change( // connect-time probe call), so RaiseClanChanged below is guaranteed to reach the handler. await WaitUntilAsync(() => !captured.IsEmpty, cts.Token); Assert.NotNull(source.LastConnection); - source.LastConnection!.RaiseClanChanged(ClanProbeResult.NoClan); + source.LastConnection.RaiseClanChanged(ClanProbeResult.NoClan); await WaitUntilAsync(() => captured.Count >= 2, cts.Token); @@ -321,7 +321,7 @@ public async Task Publishes_clan_messages_and_flags_the_active_player() await WaitUntilAsync(() => !stateCaptured.IsEmpty, cts.Token); Assert.NotNull(source.LastConnection); - source.LastConnection!.RaiseClanMessage(new ClanChatLine(100UL, "Active", "hi", DateTimeOffset.UnixEpoch)); + source.LastConnection.RaiseClanMessage(new ClanChatLine(100UL, "Active", "hi", DateTimeOffset.UnixEpoch)); source.LastConnection.RaiseClanMessage(new ClanChatLine(999UL, "Other", "yo", DateTimeOffset.UnixEpoch)); await WaitUntilAsync(() => captured.Count >= 2, cts.Token); @@ -394,7 +394,7 @@ public async Task Stops_publishing_clan_events_after_the_socket_closes() await WaitUntilAsync(() => !stateCaptured.IsEmpty, cts.Token); Assert.NotNull(source.LastConnection); - var closedConnection = source.LastConnection!; + var closedConnection = source.LastConnection; // Stop the connection loop — this joins RunConnectedAsync's finally block, which unsubscribes // the clan handlers, before StopAsync returns. @@ -453,7 +453,7 @@ public async Task Routes_a_team_send_to_team_chat_and_a_clan_send_to_clan_chat() Assert.Equal(ChatSendResult.Sent, clanResult); Assert.NotNull(source.LastConnection); - var connection = source.LastConnection!; + var connection = source.LastConnection; // Assert both destinations independently, so swapping the routing fails on both sides. Assert.Equal("[Alice] team line", Assert.Single(connection.SentMessages)); diff --git a/tests/RustPlusBot.Features.Connections.Tests/ConnectionSupervisorTests.cs b/tests/RustPlusBot.Features.Connections.Tests/ConnectionSupervisorTests.cs index 859eb728..c4dd933b 100644 --- a/tests/RustPlusBot.Features.Connections.Tests/ConnectionSupervisorTests.cs +++ b/tests/RustPlusBot.Features.Connections.Tests/ConnectionSupervisorTests.cs @@ -159,7 +159,7 @@ public async Task Connect_Healthy_BecomesConnectedWithPlayerCount() var state = await WaitForStateAsync(h.Provider, serverId, s => s.Status == ConnectionStatus.Connected); Assert.NotNull(state); - Assert.Equal(7, state!.PlayerCount); + Assert.Equal(7, state.PlayerCount); } [Fact] @@ -432,7 +432,7 @@ public async Task First_marker_poll_is_a_silent_baseline() Assert.Single(captured); Assert.True(captured.TryPeek(out var evt)); - Assert.Single(evt!.Added); + Assert.Single(evt.Added); Assert.Equal(MarkerKind.PatrolHelicopter, evt.Added[0].Kind); Assert.Empty(evt.Removed); @@ -489,7 +489,7 @@ public async Task Marker_added_on_a_later_poll_publishes_changed_event() Assert.Single(captured); Assert.True(captured.TryPeek(out var evt)); Assert.NotNull(evt); - Assert.Single(evt!.Added); + Assert.Single(evt.Added); Assert.Equal(MarkerKind.CargoShip, evt.Added[0].Kind); Assert.Empty(evt.Removed); Assert.Equal(expectedDims, evt.Dimensions); @@ -540,7 +540,7 @@ public async Task Marker_position_change_publishes_moved_bucket() Assert.Single(captured); Assert.True(captured.TryPeek(out var evt)); Assert.NotNull(evt); - Assert.Empty(evt!.Added); + Assert.Empty(evt.Added); Assert.Empty(evt.Removed); var moved = Assert.Single(evt.Moved); Assert.Equal(7UL, moved.Id); @@ -602,7 +602,7 @@ public async Task Failed_marker_poll_retains_previous_snapshot() await Task.Delay(TimeSpan.FromMilliseconds(60), cts.Token); // Poll 4: recover — hold-last still returns [cargo], so snapshot is unchanged, no new event. - source.LastConnection!.MarkersThrow = false; + source.LastConnection.MarkersThrow = false; await Task.Delay(TimeSpan.FromMilliseconds(100), cts.Token); // Still only one event total — the failed poll did not corrupt the snapshot. @@ -659,7 +659,7 @@ public async Task Ch47_entering_rig_radius_publishes_activated_once_per_visit() Assert.Single(rigEvents); Assert.True(rigEvents.TryPeek(out var evt)); Assert.NotNull(evt); - Assert.Equal(RigKind.Small, evt!.Rig); + Assert.Equal(RigKind.Small, evt.Rig); Assert.Equal(RigEventKind.Activated, evt.Kind); Assert.Equal(1000f, evt.X); Assert.Equal(1000f, evt.Y); @@ -784,7 +784,7 @@ public async Task TeamChanged_push_publishes_player_state_transition() await WaitUntilAsync(() => !captured.IsEmpty, cts.Token); Assert.True(captured.TryDequeue(out var evt)); - var transition = Assert.Single(evt!.Transitions); + var transition = Assert.Single(evt.Transitions); Assert.Equal(PlayerTransitionKind.Disconnect, transition.Kind); Assert.Equal(100UL, transition.SteamId); @@ -838,7 +838,7 @@ online with await WaitUntilAsync(() => !captured.IsEmpty, cts.Token); Assert.True(captured.TryDequeue(out var evt)); - Assert.Contains(evt!.Transitions, t => t.Kind == PlayerTransitionKind.Disconnect && t.SteamId == 100UL); + Assert.Contains(evt.Transitions, t => t.Kind == PlayerTransitionKind.Disconnect && t.SteamId == 100UL); await h.Supervisor.StopAllAsync(); await cts.CancelAsync(); diff --git a/tests/RustPlusBot.Features.Events.Tests/Messages/ServerEventsMessageRendererTests.cs b/tests/RustPlusBot.Features.Events.Tests/Messages/ServerEventsMessageRendererTests.cs index 93be0d48..0b9924de 100644 --- a/tests/RustPlusBot.Features.Events.Tests/Messages/ServerEventsMessageRendererTests.cs +++ b/tests/RustPlusBot.Features.Events.Tests/Messages/ServerEventsMessageRendererTests.cs @@ -47,7 +47,7 @@ public async Task Renders_all_five_rows() var payload = await Build(events, rigs).RenderAsync(new MessageRenderContext(1, ServerId, "en"), default); Assert.NotNull(payload.Embed); - Assert.Equal(5, payload.Embed!.Fields.Length); + Assert.Equal(5, payload.Embed.Fields.Length); } [Fact] diff --git a/tests/RustPlusBot.Features.ItemData.Tests/EmbeddedItemDatabaseTests.cs b/tests/RustPlusBot.Features.ItemData.Tests/EmbeddedItemDatabaseTests.cs index 92325263..654845ec 100644 --- a/tests/RustPlusBot.Features.ItemData.Tests/EmbeddedItemDatabaseTests.cs +++ b/tests/RustPlusBot.Features.ItemData.Tests/EmbeddedItemDatabaseTests.cs @@ -13,7 +13,7 @@ public void GetById_ReturnsKnownItem() { var ak = _db.GetById(1545779598); Assert.NotNull(ak); - Assert.Equal("Assault Rifle", ak!.Name); + Assert.Equal("Assault Rifle", ak.Name); } [Fact] @@ -61,8 +61,8 @@ public void GetById_StoneBarricade_HasDecay() { var rec = _db.GetById(15388698); Assert.NotNull(rec); - Assert.NotNull(rec!.Decay); - Assert.Equal(900, rec.Decay!.Seconds); + Assert.NotNull(rec.Decay); + Assert.Equal(900, rec.Decay.Seconds); Assert.Equal(100, rec.Decay.Hp); } @@ -71,8 +71,8 @@ public void GetById_WoodenDoor_HasUpkeep() { var rec = _db.GetById(1729120840); Assert.NotNull(rec); - Assert.NotNull(rec!.Upkeep); - var entry = Assert.Single(rec.Upkeep!.Entries); + Assert.NotNull(rec.Upkeep); + var entry = Assert.Single(rec.Upkeep.Entries); Assert.Equal(-151838493, entry.ItemId); Assert.Equal(30, entry.QuantityMin); Assert.Equal(100, entry.QuantityMax); diff --git a/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs b/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs index fe84a1ee..19923e13 100644 --- a/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs @@ -99,7 +99,7 @@ public async Task Renders_a_png_when_base_map_available() var result = await composer.ComposeAsync(Guild, Server, CancellationToken.None); Assert.NotNull(result); - using var image = Image.Load(result!.Png); + using var image = Image.Load(result.Png); Assert.Equal(MapRenderer.OutputSize, image.Width); } @@ -114,7 +114,7 @@ [new TrailPoint(2000f, 2000f)], null)), var result = await composer.ComposeAsync(Guild, Server, CancellationToken.None); Assert.NotNull(result); - using var image = Image.Load(result!.Png); + using var image = Image.Load(result.Png); Assert.Equal(MapRenderer.OutputSize, image.Width); } @@ -158,10 +158,10 @@ public async Task Tunnel_tokens_render_under_the_tunnels_layer_not_the_monuments Assert.NotNull(monResult); Assert.NotNull(tunResult); // Monuments layer must NOT draw the tunnel token -> identical to the bare base render. - Assert.True(monResult!.Png.SequenceEqual(baseResult!.Png), + Assert.True(monResult.Png.SequenceEqual(baseResult.Png), "tunnel token must be excluded from the Monuments pass"); // Tunnels layer MUST draw the tunnel token -> differs from the bare base render. - Assert.False(tunResult!.Png.SequenceEqual(baseResult!.Png), "tunnel token must render under the Tunnels layer"); + Assert.False(tunResult.Png.SequenceEqual(baseResult.Png), "tunnel token must render under the Tunnels layer"); } private static async Task ComposeTunnelScenarioAsync(bool monuments, bool tunnels) @@ -219,7 +219,7 @@ public async Task Renders_the_grid_even_with_no_markers() Assert.NotNull(resultOn); Assert.NotNull(resultOff); // The grid layer must have painted at least one pixel differently. - Assert.False(resultOn!.Png.SequenceEqual(resultOff!.Png), "Grid-on and grid-off renders must differ."); + Assert.False(resultOn.Png.SequenceEqual(resultOff.Png), "Grid-on and grid-off renders must differ."); } [Fact] @@ -247,7 +247,7 @@ public async Task ComposeAsync_forwards_vendor_marker_history_into_rendered_trai Assert.NotNull(resultNoTrail); // MapRenderer.DrawTrails only paints when Trail.Count >= 2, so a 2-point History must render // differently from a 1-point History — proving the history ring made it through to the trail. - Assert.False(resultWithTrail!.Png.SequenceEqual(resultNoTrail!.Png), + Assert.False(resultWithTrail.Png.SequenceEqual(resultNoTrail.Png), "Vendor trail with 2-point history must render differently than a 1-point history."); } @@ -267,9 +267,9 @@ public async Task ComposeAsync_builds_a_legend_entry_per_player() var result = await composer.ComposeAsync(Guild, Server, CancellationToken.None); Assert.NotNull(result); - Assert.NotNull(result!.Legend); + Assert.NotNull(result.Legend); // Ordered by SteamId: Ada (10) first, Bob (20) second. - Assert.Collection(result.Legend!.Entries, + Assert.Collection(result.Legend.Entries, e => { Assert.Equal("Ada", e.Name); diff --git a/tests/RustPlusBot.Features.Map.Tests/MapIconsTests.cs b/tests/RustPlusBot.Features.Map.Tests/MapIconsTests.cs index 0727162e..69a3e7f5 100644 --- a/tests/RustPlusBot.Features.Map.Tests/MapIconsTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/MapIconsTests.cs @@ -18,7 +18,7 @@ public void Sized_marker_icon_fits_the_requested_box() var icon = MapIcons.Marker(MarkerKind.CargoShip, 40); Assert.NotNull(icon); - Assert.True(icon!.Width <= 40 && icon.Height <= 40); + Assert.True(icon.Width <= 40 && icon.Height <= 40); Assert.True(icon.Width == 40 || icon.Height == 40); // aspect-preserving fit, longest edge = size } diff --git a/tests/RustPlusBot.Features.Map.Tests/MapLegendEmbedTests.cs b/tests/RustPlusBot.Features.Map.Tests/MapLegendEmbedTests.cs index e1600058..6b6ed6b1 100644 --- a/tests/RustPlusBot.Features.Map.Tests/MapLegendEmbedTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/MapLegendEmbedTests.cs @@ -24,7 +24,7 @@ public void Build_lists_one_line_per_player() var embed = MapLegendEmbed.Build(legend); Assert.NotNull(embed); - Assert.Contains("Ada", embed!.Description, StringComparison.Ordinal); + Assert.Contains("Ada", embed.Description, StringComparison.Ordinal); Assert.Contains("online", embed.Description, StringComparison.Ordinal); Assert.Contains("Bob", embed.Description, StringComparison.Ordinal); Assert.Contains("🟦", embed.Description, StringComparison.Ordinal); diff --git a/tests/RustPlusBot.Features.Map.Tests/MonumentIconSourceTests.cs b/tests/RustPlusBot.Features.Map.Tests/MonumentIconSourceTests.cs index 81ee8c17..bc39c555 100644 --- a/tests/RustPlusBot.Features.Map.Tests/MonumentIconSourceTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/MonumentIconSourceTests.cs @@ -20,7 +20,7 @@ public void Monument_rasterizes_known_token_at_requested_size() var icon = CreateSource().Monument("launchsite", 30); Assert.NotNull(icon); - Assert.Equal(30, icon!.Width); + Assert.Equal(30, icon.Width); Assert.Equal(30, icon.Height); } @@ -77,7 +77,7 @@ public void Every_known_token_rasterizes() Assert.True(icon is not null, $"{token} produced no icon"); var hasVisiblePixel = false; - for (var y = 0; y < icon!.Height && !hasVisiblePixel; y++) + for (var y = 0; y < icon.Height && !hasVisiblePixel; y++) { for (var x = 0; x < icon.Width; x++) { @@ -137,7 +137,7 @@ private static MonumentAsset CreateBogusAsset() var ctor = typeof(MonumentAsset).GetConstructor( BindingFlags.Instance | BindingFlags.NonPublic, [typeof(MonumentType), typeof(string)]); Assert.NotNull(ctor); - return (MonumentAsset)ctor!.Invoke([MonumentType.LaunchSite, "Definitely_Not_An_Asset"]); + return (MonumentAsset)ctor.Invoke([MonumentType.LaunchSite, "Definitely_Not_An_Asset"]); } private sealed class RecordingLogger : ILogger diff --git a/tests/RustPlusBot.Features.Map.Tests/RustMaps/RustMapsMapCoordinatorTests.cs b/tests/RustPlusBot.Features.Map.Tests/RustMaps/RustMapsMapCoordinatorTests.cs index d4b6237d..4fe6fda0 100644 --- a/tests/RustPlusBot.Features.Map.Tests/RustMaps/RustMapsMapCoordinatorTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/RustMaps/RustMapsMapCoordinatorTests.cs @@ -65,7 +65,7 @@ public void GetReady_returns_the_view_when_ready() var view = c.GetReady(Key.Size, Key.Seed); Assert.NotNull(view); - Assert.Equal("https://img/x.png", view!.ImageUrl); + Assert.Equal("https://img/x.png", view.ImageUrl); Assert.Equal("https://rustmaps/x", view.RustMapsPageUrl); } diff --git a/tests/RustPlusBot.Features.Map.Tests/RustPlusBaseMapSourceTests.cs b/tests/RustPlusBot.Features.Map.Tests/RustPlusBaseMapSourceTests.cs index 1a29a39d..0b7c5176 100644 --- a/tests/RustPlusBot.Features.Map.Tests/RustPlusBaseMapSourceTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/RustPlusBaseMapSourceTests.cs @@ -21,7 +21,7 @@ public async Task GetAsync_returns_image_when_bytes_and_dimensions_present() var result = await source.GetAsync(Guild, Server, CancellationToken.None); Assert.NotNull(result); - Assert.Equal((byte[])[1, 2, 3], result!.Bytes); + Assert.Equal((byte[])[1, 2, 3], result.Bytes); Assert.Equal(1000, result.PixelWidth); Assert.Equal(1000, result.PixelHeight); Assert.Equal(50, result.OceanMarginPx); diff --git a/tests/RustPlusBot.Features.Pairing.Tests/PairingSupervisorTests.cs b/tests/RustPlusBot.Features.Pairing.Tests/PairingSupervisorTests.cs index 50d61ef2..2540eaa2 100644 --- a/tests/RustPlusBot.Features.Pairing.Tests/PairingSupervisorTests.cs +++ b/tests/RustPlusBot.Features.Pairing.Tests/PairingSupervisorTests.cs @@ -134,7 +134,8 @@ public async Task ConnectedListener_DeliversNotificationWithoutFaulting() await h.Supervisor.EnsureListenerAsync(10UL, 99UL); var note = new PairingNotification(PairingKind.Server, "S", "1.2.3.4", 28015, 7UL, "tok"); - await h.Source.LastCallback!(note, CancellationToken.None); + Assert.NotNull(h.Source.LastCallback); + await h.Source.LastCallback(note, CancellationToken.None); using var scope = h.Provider.CreateScope(); var reg = await scope.ServiceProvider.GetRequiredService().GetAsync(10UL, 99UL); diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Locating/TeamChatChannelLocatorTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Locating/TeamChatChannelLocatorTests.cs index da96f408..72fce1a8 100644 --- a/tests/RustPlusBot.Features.Workspace.Tests/Locating/TeamChatChannelLocatorTests.cs +++ b/tests/RustPlusBot.Features.Workspace.Tests/Locating/TeamChatChannelLocatorTests.cs @@ -85,7 +85,7 @@ public async Task ResolveAsync_maps_channel_to_guild_and_server() var resolved = await locator.ResolveAsync(777UL, CancellationToken.None); Assert.NotNull(resolved); - Assert.Equal(10UL, resolved!.Value.GuildId); + Assert.Equal(10UL, resolved.Value.GuildId); Assert.Equal(serverId, resolved.Value.ServerId); } diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs index 98cf0eeb..ae81bb98 100644 --- a/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs +++ b/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs @@ -13,11 +13,15 @@ public sealed class MapControlMessageRendererTests { private static readonly ResxLocalizer Loc = new(); - private static List Buttons(MessagePayload payload) => - [ - .. payload.Components!.Components.OfType() - .SelectMany(r => r.Components).OfType(), - ]; + private static List Buttons(MessagePayload payload) + { + Assert.NotNull(payload.Components); + return + [ + .. payload.Components.Components.OfType() + .SelectMany(r => r.Components).OfType(), + ]; + } [Fact] public async Task Renders_seven_toggle_buttons_reflecting_settings() diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs index c8201ace..25ca3820 100644 --- a/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs +++ b/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs @@ -45,7 +45,7 @@ public async Task Information_ShowsServerCount() var payload = await renderer.RenderAsync(Global, default); Assert.NotNull(payload.Embed); - Assert.Contains("3", payload.Embed!.Description, StringComparison.Ordinal); + Assert.Contains("3", payload.Embed.Description, StringComparison.Ordinal); } [Fact] @@ -58,7 +58,7 @@ public async Task Settings_HasLanguageSelectMenu() var payload = await renderer.RenderAsync(Global, default); Assert.NotNull(payload.Components); - var selects = payload.Components!.Components.OfType().SelectMany(r => r.Components) + var selects = payload.Components.Components.OfType().SelectMany(r => r.Components) .OfType(); Assert.Contains(selects, s => s.CustomId == "workspace:settings:culture"); } @@ -299,7 +299,7 @@ public async Task Setup_HasConnectAccountButton() var payload = await renderer.RenderAsync(Global, default); Assert.NotNull(payload.Components); - var buttons = payload.Components!.Components.OfType() + var buttons = payload.Components.Components.OfType() .SelectMany(r => r.Components).OfType(); Assert.Contains(buttons, b => b.CustomId == "workspace:setup:connect"); } diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Messages/ServerInfoMapMessageRendererTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Messages/ServerInfoMapMessageRendererTests.cs index 19fc7fdd..2583b0cf 100644 --- a/tests/RustPlusBot.Features.Workspace.Tests/Messages/ServerInfoMapMessageRendererTests.cs +++ b/tests/RustPlusBot.Features.Workspace.Tests/Messages/ServerInfoMapMessageRendererTests.cs @@ -44,7 +44,7 @@ public async Task Ready_view_sets_the_embed_image_and_url() var payload = await renderer.RenderAsync(new MessageRenderContext(1, serverId, "en"), default); Assert.NotNull(payload.Embed); - Assert.Equal("https://img/icons.png", payload.Embed!.Image!.Value.Url); + Assert.Equal("https://img/icons.png", payload.Embed.Image!.Value.Url); Assert.Equal("https://rustmaps/x", payload.Embed.Url); } diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerMessageTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerMessageTests.cs index d1670bd1..967134d1 100644 --- a/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerMessageTests.cs +++ b/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerMessageTests.cs @@ -70,7 +70,7 @@ public async Task NewEarlierMessageBehindLiveOne_DeletesAndRepostsInDeclarationO var originalInfoMessage = await harness.Store.GetMessageAsync(1, serverId, "server.info"); Assert.NotNull(originalInfoMessage); - var originalInfoMessageId = originalInfoMessage!.DiscordMessageId; + var originalInfoMessageId = originalInfoMessage.DiscordMessageId; Assert.Equal(1, harness.Gateway.PostedMessages); Assert.Empty(harness.Gateway.DeletedMessageIds); @@ -101,7 +101,7 @@ public async Task NewEarlierMessageBehindLiveOne_DeletesAndRepostsInDeclarationO var infoRecord = await harness.Store.GetMessageAsync(1, serverId, "server.info"); Assert.NotNull(mapRecord); Assert.NotNull(infoRecord); - Assert.NotEqual(originalInfoMessageId, infoRecord!.DiscordMessageId); + Assert.NotEqual(originalInfoMessageId, infoRecord.DiscordMessageId); } [Fact] diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerServerTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerServerTests.cs index 47b4de0b..67d0b841 100644 --- a/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerServerTests.cs +++ b/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerServerTests.cs @@ -30,7 +30,7 @@ public async Task ReconcileServer_CreatesCategoryNamedAfterServer_ScopedToServer Assert.Equal(ReconcileStatus.Provisioned, result.Status); var category = await harness.Store.GetCategoryAsync(1, serverId); Assert.NotNull(category); - Assert.Equal(category!.DiscordCategoryId, await harness.Gateway.FindCategoryAsync(1, "Rustopia EU", default)); + Assert.Equal(category.DiscordCategoryId, await harness.Gateway.FindCategoryAsync(1, "Rustopia EU", default)); var channels = await harness.Store.GetChannelsAsync(1, serverId); Assert.Single(channels); Assert.Equal(serverId, channels[0].RustServerId); diff --git a/tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreTests.cs b/tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreTests.cs index f533f34d..0d1f0a4d 100644 --- a/tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreTests.cs +++ b/tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreTests.cs @@ -31,7 +31,7 @@ await store.SaveCategoryAsync(new ProvisionedCategory var loaded = await store.GetCategoryAsync(1, null); Assert.NotNull(loaded); - Assert.Equal(20UL, loaded!.DiscordCategoryId); + Assert.Equal(20UL, loaded.DiscordCategoryId); } [Fact] @@ -78,7 +78,7 @@ await store.SaveMessageAsync(new ProvisionedMessage var loaded = await store.GetMessageAsync(1, null, "information.main"); Assert.NotNull(loaded); - Assert.Equal(101UL, loaded!.DiscordMessageId); + Assert.Equal(101UL, loaded.DiscordMessageId); Assert.Equal(DateTimeOffset.UnixEpoch, loaded.UpdatedAt); }