diff --git a/Content.Benchmarks/DeviceNetworkingBenchmark.cs b/Content.Benchmarks/DeviceNetworkingBenchmark.cs
index 8af7f2b262..8aeddd6304 100644
--- a/Content.Benchmarks/DeviceNetworkingBenchmark.cs
+++ b/Content.Benchmarks/DeviceNetworkingBenchmark.cs
@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
@@ -16,7 +17,7 @@ namespace Content.Benchmarks;
[MemoryDiagnoser]
public class DeviceNetworkingBenchmark
{
- private PairTracker _pair = default!;
+ private TestPair _pair = default!;
private DeviceNetworkTestSystem _deviceNetTestSystem = default!;
private DeviceNetworkSystem _deviceNetworkSystem = default!;
private EntityUid _sourceEntity;
diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index bd4213e438..15cbf96c36 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
using Content.Server.Maps;
using Robust.Server.GameObjects;
using Robust.Shared;
@@ -17,7 +18,7 @@ namespace Content.Benchmarks;
[Virtual]
public class MapLoadBenchmark
{
- private PairTracker _pair = default!;
+ private TestPair _pair = default!;
private MapLoaderSystem _mapLoader = default!;
private IMapManager _mapManager = default!;
diff --git a/Content.IntegrationTests/Pair/TestMapData.cs b/Content.IntegrationTests/Pair/TestMapData.cs
new file mode 100644
index 0000000000..62fefd8722
--- /dev/null
+++ b/Content.IntegrationTests/Pair/TestMapData.cs
@@ -0,0 +1,19 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.IntegrationTests.Pair;
+
+///
+/// Simple data class that stored information about a map being used by a test.
+///
+public sealed class TestMapData
+{
+ public EntityUid MapUid { get; set; }
+ public EntityUid GridUid { get; set; }
+ public MapId MapId { get; set; }
+ public MapGridComponent MapGrid { get; set; } = default!;
+ public EntityCoordinates GridCoords { get; set; }
+ public MapCoordinates MapCoords { get; set; }
+ public TileRef Tile { get; set; }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
new file mode 100644
index 0000000000..fc48bfec30
--- /dev/null
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -0,0 +1,39 @@
+#nullable enable
+using System.Linq;
+using Robust.Shared.Map;
+
+namespace Content.IntegrationTests.Pair;
+
+// Contains misc helper functions to make writing tests easier.
+public sealed partial class TestPair
+{
+ ///
+ /// Creates a map, a grid, and a tile, and gives back references to them.
+ ///
+ public async Task CreateTestMap()
+ {
+ await Server.WaitIdleAsync();
+ var tileDefinitionManager = Server.ResolveDependency();
+
+ var mapData = new TestMapData();
+ await Server.WaitPost(() =>
+ {
+ mapData.MapId = Server.MapMan.CreateMap();
+ mapData.MapUid = Server.MapMan.GetMapEntityId(mapData.MapId);
+ mapData.MapGrid = Server.MapMan.CreateGrid(mapData.MapId);
+ mapData.GridUid = mapData.MapGrid.Owner; // Fixing this requires an engine PR.
+ mapData.GridCoords = new EntityCoordinates(mapData.GridUid, 0, 0);
+ var plating = tileDefinitionManager["Plating"];
+ var platingTile = new Tile(plating.TileId);
+ mapData.MapGrid.SetTile(mapData.GridCoords, platingTile);
+ mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
+ mapData.Tile = mapData.MapGrid.GetAllTiles().First();
+ });
+
+ if (Settings.Connected)
+ await RunTicksSync(10);
+
+ TestMap = mapData;
+ return mapData;
+ }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/Pair/TestPair.Prototypes.cs b/Content.IntegrationTests/Pair/TestPair.Prototypes.cs
new file mode 100644
index 0000000000..35893f6782
--- /dev/null
+++ b/Content.IntegrationTests/Pair/TestPair.Prototypes.cs
@@ -0,0 +1,64 @@
+#nullable enable
+using System.Collections.Generic;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using Robust.UnitTesting;
+
+namespace Content.IntegrationTests.Pair;
+
+// This partial class contains helper methods to deal with yaml prototypes.
+public sealed partial class TestPair
+{
+ private Dictionary> _loadedPrototypes = new();
+ private HashSet _loadedEntityPrototypes = new();
+
+ public async Task LoadPrototypes(List prototypes)
+ {
+ await LoadPrototypes(Server, prototypes);
+ await LoadPrototypes(Client, prototypes);
+ }
+
+ private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List prototypes)
+ {
+ var changed = new Dictionary>();
+ foreach (var file in prototypes)
+ {
+ instance.ProtoMan.LoadString(file, changed: changed);
+ }
+
+ await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
+
+ foreach (var (kind, ids) in changed)
+ {
+ _loadedPrototypes.GetOrNew(kind).UnionWith(ids);
+ }
+
+ if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
+ _loadedEntityPrototypes.UnionWith(entIds);
+ }
+
+ public bool IsTestPrototype(EntityPrototype proto)
+ {
+ return _loadedEntityPrototypes.Contains(proto.ID);
+ }
+
+ public bool IsTestEntityPrototype(string id)
+ {
+ return _loadedEntityPrototypes.Contains(id);
+ }
+
+ public bool IsTestPrototype(string id) where TPrototype : IPrototype
+ {
+ return IsTestPrototype(typeof(TPrototype), id);
+ }
+
+ public bool IsTestPrototype(TPrototype proto) where TPrototype : IPrototype
+ {
+ return IsTestPrototype(typeof(TPrototype), proto.ID);
+ }
+
+ public bool IsTestPrototype(Type kind, string id)
+ {
+ return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
+ }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
new file mode 100644
index 0000000000..bdd4fc7791
--- /dev/null
+++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
@@ -0,0 +1,218 @@
+#nullable enable
+using System.IO;
+using System.Linq;
+using Content.Server.GameTicking;
+using Content.Server.Mind.Components;
+using Content.Shared.CCVar;
+using Content.Shared.GameTicking;
+using Robust.Client;
+using Robust.Server.Player;
+using Robust.Shared.Exceptions;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Network;
+
+namespace Content.IntegrationTests.Pair;
+
+// This partial class contains logic related to recycling & disposing test pairs.
+public sealed partial class TestPair : IAsyncDisposable
+{
+ public PairState State { get; private set; } = PairState.Ready;
+
+ private async Task OnDirtyDispose()
+ {
+ var usageTime = Watch.Elapsed;
+ Watch.Restart();
+ await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
+ Kill();
+ var disposeTime = Watch.Elapsed;
+ await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
+ // Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
+ // because someone forgot to clean-return the pair.
+ Assert.Warn("Test was dirty-disposed.");
+ }
+
+ private async Task OnCleanDispose()
+ {
+ if (TestMap != null)
+ {
+ await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
+ TestMap = null;
+ }
+
+ var usageTime = Watch.Elapsed;
+ Watch.Restart();
+ await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
+ // Let any last minute failures the test cause happen.
+ await ReallyBeIdle();
+ if (!Settings.Destructive)
+ {
+ if (Client.IsAlive == false)
+ {
+ throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);
+ }
+
+ if (Server.IsAlive == false)
+ {
+ throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
+ }
+ }
+
+ if (Settings.MustNotBeReused)
+ {
+ Kill();
+ await ReallyBeIdle();
+ await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
+ return;
+ }
+
+ var sRuntimeLog = Server.ResolveDependency();
+ if (sRuntimeLog.ExceptionCount > 0)
+ throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
+ var cRuntimeLog = Client.ResolveDependency();
+ if (cRuntimeLog.ExceptionCount > 0)
+ throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
+
+ var returnTime = Watch.Elapsed;
+ await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
+ }
+
+ public async ValueTask CleanReturnAsync()
+ {
+ if (State != PairState.InUse)
+ throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
+
+ await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
+ State = PairState.CleanDisposed;
+ await OnCleanDispose();
+ State = PairState.Ready;
+ PoolManager.NoCheckReturn(this);
+ ClearContext();
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ switch (State)
+ {
+ case PairState.Dead:
+ case PairState.Ready:
+ break;
+ case PairState.InUse:
+ await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
+ await OnDirtyDispose();
+ PoolManager.NoCheckReturn(this);
+ ClearContext();
+ break;
+ default:
+ throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
+ }
+ }
+
+ public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
+ {
+ Settings = default!;
+ Watch.Restart();
+ await testOut.WriteLineAsync($"Recycling...");
+
+ var gameTicker = Server.System();
+ var cNetMgr = Client.ResolveDependency();
+
+ await RunTicksSync(1);
+
+ // Disconnect the client if they are connected.
+ if (cNetMgr.IsConnected)
+ {
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
+ await Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
+ await RunTicksSync(1);
+ }
+ Assert.That(cNetMgr.IsConnected, Is.False);
+
+ // Move to pre-round lobby. Required to toggle dummy ticker on and off
+ if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
+ {
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server.");
+ Assert.That(gameTicker.DummyTicker, Is.False);
+ Server.CfgMan.SetCVar(CCVars.GameLobbyEnabled, true);
+ await Server.WaitPost(() => gameTicker.RestartRound());
+ await RunTicksSync(1);
+ }
+
+ //Apply Cvars
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
+ await PoolManager.SetupCVars(Client, settings);
+ await PoolManager.SetupCVars(Server, settings);
+ await RunTicksSync(1);
+
+ // Restart server.
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server again");
+ await Server.WaitPost(() => gameTicker.RestartRound());
+ await RunTicksSync(1);
+
+ // Connect client
+ if (settings.ShouldBeConnected)
+ {
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
+ Client.SetConnectTarget(Server);
+ await Client.WaitPost(() => cNetMgr.ClientConnect(null!, 0, null!));
+ }
+
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
+ await ReallyBeIdle();
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
+ }
+
+ public void ValidateSettings(PoolSettings settings)
+ {
+ var cfg = Server.CfgMan;
+ Assert.That(cfg.GetCVar(CCVars.AdminLogsEnabled), Is.EqualTo(settings.AdminLogsEnabled));
+ Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby));
+ Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.UseDummyTicker));
+
+ var entMan = Server.ResolveDependency();
+ var ticker = entMan.System();
+ Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
+
+ var expectPreRound = settings.InLobby | settings.DummyTicker;
+ var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
+ Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
+
+ var baseClient = Client.ResolveDependency();
+ var netMan = Client.ResolveDependency();
+ Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
+
+ if (!settings.ShouldBeConnected)
+ return;
+
+ Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
+ var cPlayer = Client.ResolveDependency();
+ var sPlayer = Server.ResolveDependency();
+ Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
+ var session = sPlayer.Sessions.Single();
+ Assert.That(cPlayer.LocalPlayer?.Session.UserId, Is.EqualTo(session.UserId));
+
+ if (ticker.DummyTicker)
+ return;
+
+ var status = ticker.PlayerGameStatuses[session.UserId];
+ var expected = settings.InLobby
+ ? PlayerGameStatus.NotReadyToPlay
+ : PlayerGameStatus.JoinedGame;
+
+ Assert.That(status, Is.EqualTo(expected));
+
+ if (settings.InLobby)
+ {
+ Assert.Null(session.AttachedEntity);
+ return;
+ }
+
+ Assert.NotNull(session.AttachedEntity);
+ Assert.That(entMan.EntityExists(session.AttachedEntity));
+ Assert.That(entMan.HasComponent(session.AttachedEntity));
+ var mindCont = entMan.GetComponent(session.AttachedEntity!.Value);
+ Assert.NotNull(mindCont.Mind);
+ Assert.Null(mindCont.Mind?.VisitingEntity);
+ Assert.That(mindCont.Mind!.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
+ Assert.That(mindCont.Mind.UserId, Is.EqualTo(session.UserId));
+ }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/Pair/TestPair.Timing.cs b/Content.IntegrationTests/Pair/TestPair.Timing.cs
new file mode 100644
index 0000000000..3487ea6801
--- /dev/null
+++ b/Content.IntegrationTests/Pair/TestPair.Timing.cs
@@ -0,0 +1,62 @@
+#nullable enable
+using Robust.Shared.Timing;
+
+namespace Content.IntegrationTests.Pair;
+
+// This partial class contains methods for running the server/client pairs for some number of ticks
+public sealed partial class TestPair
+{
+ ///
+ /// Runs the server-client pair in sync
+ ///
+ /// How many ticks to run them for
+ public async Task RunTicksSync(int ticks)
+ {
+ for (var i = 0; i < ticks; i++)
+ {
+ await Server.WaitRunTicks(1);
+ await Client.WaitRunTicks(1);
+ }
+ }
+
+ ///
+ /// Runs the server-client pair in sync, but also ensures they are both idle each tick.
+ ///
+ /// How many ticks to run
+ public async Task ReallyBeIdle(int runTicks = 25)
+ {
+ for (var i = 0; i < runTicks; i++)
+ {
+ await Client.WaitRunTicks(1);
+ await Server.WaitRunTicks(1);
+ for (var idleCycles = 0; idleCycles < 4; idleCycles++)
+ {
+ await Client.WaitIdleAsync();
+ await Server.WaitIdleAsync();
+ }
+ }
+ }
+
+ ///
+ /// Run the server/clients until the ticks are synchronized.
+ /// By default the client will be one tick ahead of the server.
+ ///
+ public async Task SyncTicks(int targetDelta = 1)
+ {
+ var sTick = (int)Server.Timing.CurTick.Value;
+ var cTick = (int)Client.Timing.CurTick.Value;
+ var delta = cTick - sTick;
+
+ if (delta == targetDelta)
+ return;
+ if (delta > targetDelta)
+ await Server.WaitRunTicks(delta - targetDelta);
+ else
+ await Client.WaitRunTicks(targetDelta - delta);
+
+ sTick = (int)Server.Timing.CurTick.Value;
+ cTick = (int)Client.Timing.CurTick.Value;
+ delta = cTick - sTick;
+ Assert.That(delta, Is.EqualTo(targetDelta));
+ }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs
new file mode 100644
index 0000000000..41af4b2c80
--- /dev/null
+++ b/Content.IntegrationTests/Pair/TestPair.cs
@@ -0,0 +1,111 @@
+#nullable enable
+using System.Collections.Generic;
+using System.IO;
+using Content.Server.GameTicking;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Network;
+using Robust.Shared.Timing;
+using Robust.UnitTesting;
+
+namespace Content.IntegrationTests.Pair;
+
+///
+/// This object wraps a pooled server+client pair.
+///
+public sealed partial class TestPair
+{
+ // TODO remove this.
+ [Obsolete("Field access is redundant")]
+ public TestPair Pair => this;
+
+ public readonly int Id;
+ private bool _initialized;
+ private TextWriter _testOut = default!;
+ public readonly Stopwatch Watch = new();
+ public readonly List TestHistory = new();
+ public PoolSettings Settings = default!;
+ public TestMapData? TestMap;
+ public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
+ public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
+
+ public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
+ public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
+
+ public TestPair(int id)
+ {
+ Id = id;
+ }
+
+ public async Task Initialize(PoolSettings settings, TextWriter testOut, List testPrototypes)
+ {
+ if (_initialized)
+ throw new InvalidOperationException("Already initialized");
+
+ _initialized = true;
+ Settings = settings;
+ (Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
+ (Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
+ ActivateContext(testOut);
+
+ if (!settings.NoLoadTestPrototypes)
+ await LoadPrototypes(testPrototypes!);
+
+ if (!settings.UseDummyTicker)
+ {
+ var gameTicker = Server.ResolveDependency().System();
+ await Server.WaitPost(() => gameTicker.RestartRound());
+ }
+
+ if (settings.ShouldBeConnected)
+ {
+ Client.SetConnectTarget(Server);
+ await Client.WaitPost(() =>
+ {
+ var netMgr = IoCManager.Resolve();
+ if (!netMgr.IsConnected)
+ {
+ netMgr.ClientConnect(null!, 0, null!);
+ }
+ });
+ await ReallyBeIdle(10);
+ await Client.WaitRunTicks(1);
+ }
+ }
+
+ public void Kill()
+ {
+ State = PairState.Dead;
+ Server.Dispose();
+ Client.Dispose();
+ }
+
+ private void ClearContext()
+ {
+ _testOut = default!;
+ ServerLogHandler.ClearContext();
+ ClientLogHandler.ClearContext();
+ }
+
+ public void ActivateContext(TextWriter testOut)
+ {
+ _testOut = testOut;
+ ServerLogHandler.ActivateContext(testOut);
+ ClientLogHandler.ActivateContext(testOut);
+ }
+
+ public void Use()
+ {
+ if (State != PairState.Ready)
+ throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
+ State = PairState.InUse;
+ }
+
+ public enum PairState : byte
+ {
+ Ready = 0,
+ InUse = 1,
+ CleanDisposed = 2,
+ Dead = 3,
+ }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
new file mode 100644
index 0000000000..dfdbddd923
--- /dev/null
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -0,0 +1,70 @@
+#nullable enable
+using Content.Shared.CCVar;
+using Robust.Shared;
+using Robust.Shared.Configuration;
+using Robust.UnitTesting;
+
+namespace Content.IntegrationTests;
+
+// Partial class containing cvar logic
+public static partial class PoolManager
+{
+ private static readonly (string cvar, string value)[] TestCvars =
+ {
+ // @formatter:off
+ (CCVars.DatabaseSynchronous.Name, "true"),
+ (CCVars.DatabaseSqliteDelay.Name, "0"),
+ (CCVars.HolidaysEnabled.Name, "false"),
+ (CCVars.GameMap.Name, TestMap),
+ (CCVars.AdminLogsQueueSendDelay.Name, "0"),
+ (CVars.NetPVS.Name, "false"),
+ (CCVars.NPCMaxUpdates.Name, "999999"),
+ (CVars.ThreadParallelCount.Name, "1"),
+ (CCVars.GameRoleTimers.Name, "false"),
+ (CCVars.GridFill.Name, "false"),
+ (CCVars.ArrivalsShuttles.Name, "false"),
+ (CCVars.EmergencyShuttleEnabled.Name, "false"),
+ (CCVars.ProcgenPreload.Name, "false"),
+ (CCVars.WorldgenEnabled.Name, "false"),
+ (CVars.ReplayClientRecordingEnabled.Name, "false"),
+ (CVars.ReplayServerRecordingEnabled.Name, "false"),
+ (CCVars.GameDummyTicker.Name, "true"),
+ (CCVars.GameLobbyEnabled.Name, "false"),
+ (CCVars.ConfigPresetDevelopment.Name, "false"),
+ (CCVars.AdminLogsEnabled.Name, "false"),
+ (CVars.NetBufferSize.Name, "0")
+ };
+
+ public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
+ {
+ var cfg = instance.ResolveDependency();
+ await instance.WaitPost(() =>
+ {
+ if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
+ cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
+
+ if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
+ cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
+
+ if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
+ cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
+
+ if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
+ cfg.SetCVar(CCVars.GameMap, settings.Map);
+
+ if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
+ cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
+
+ if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
+ cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
+ });
+ }
+
+ private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
+ {
+ foreach (var (cvar, value) in TestCvars)
+ {
+ options.CVarOverrides[cvar] = value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs
index 64f94e136b..af89bce99d 100644
--- a/Content.IntegrationTests/PoolManager.cs
+++ b/Content.IntegrationTests/PoolManager.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading;
using Content.Client.IoC;
using Content.Client.Parallax.Managers;
+using Content.IntegrationTests.Pair;
using Content.IntegrationTests.Tests;
using Content.IntegrationTests.Tests.Destructible;
using Content.IntegrationTests.Tests.DeviceNetwork;
@@ -16,21 +17,19 @@ using Content.Server.Mind.Components;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Robust.Client;
+using Robust.Client.State;
using Robust.Server;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
-using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
-using Robust.Shared.Utility;
using Robust.UnitTesting;
[assembly: LevelOfParallelism(3)]
@@ -43,48 +42,16 @@ namespace Content.IntegrationTests;
public static partial class PoolManager
{
public const string TestMap = "Empty";
-
- private static readonly (string cvar, string value)[] TestCvars =
- {
- // @formatter:off
- (CCVars.DatabaseSynchronous.Name, "true"),
- (CCVars.DatabaseSqliteDelay.Name, "0"),
- (CCVars.HolidaysEnabled.Name, "false"),
- (CCVars.GameMap.Name, TestMap),
- (CCVars.AdminLogsQueueSendDelay.Name, "0"),
- (CVars.NetPVS.Name, "false"),
- (CCVars.NPCMaxUpdates.Name, "999999"),
- (CVars.ThreadParallelCount.Name, "1"),
- (CCVars.GameRoleTimers.Name, "false"),
- (CCVars.GridFill.Name, "false"),
- (CCVars.ArrivalsShuttles.Name, "false"),
- (CCVars.EmergencyShuttleEnabled.Name, "false"),
- (CCVars.ProcgenPreload.Name, "false"),
- (CCVars.WorldgenEnabled.Name, "false"),
- (CVars.ReplayClientRecordingEnabled.Name, "false"),
- (CVars.ReplayServerRecordingEnabled.Name, "false"),
- (CCVars.GameDummyTicker.Name, "true"),
- (CCVars.GameLobbyEnabled.Name, "false"),
- (CCVars.ConfigPresetDevelopment.Name, "false"),
- (CCVars.AdminLogsEnabled.Name, "false"),
-
- // This breaks some tests.
- // TODO: Figure out which tests this breaks.
- (CVars.NetBufferSize.Name, "0")
-
- // @formatter:on
- };
-
private static int _pairId;
private static readonly object PairLock = new();
private static bool _initialized;
// Pair, IsBorrowed
- private static readonly Dictionary Pairs = new();
+ private static readonly Dictionary Pairs = new();
private static bool _dead;
private static Exception? _poolFailureReason;
- private static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
+ public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
PoolSettings poolSettings,
TextWriter testOut)
{
@@ -134,7 +101,7 @@ public static partial class PoolManager
///
public static void Shutdown()
{
- List localPairs;
+ List localPairs;
lock (PairLock)
{
if (_dead)
@@ -156,11 +123,11 @@ public static partial class PoolManager
lock (PairLock)
{
var builder = new StringBuilder();
- var pairs = Pairs.Keys.OrderBy(pair => pair.PairId);
+ var pairs = Pairs.Keys.OrderBy(pair => pair.Id);
foreach (var pair in pairs)
{
var borrowed = Pairs[pair];
- builder.AppendLine($"Pair {pair.PairId}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
+ builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
for (var i = 0; i < pair.TestHistory.Count; i++)
{
builder.AppendLine($"#{i}: {pair.TestHistory[i]}");
@@ -171,7 +138,7 @@ public static partial class PoolManager
}
}
- private static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
+ public static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
PoolSettings poolSettings,
TextWriter testOut)
{
@@ -225,45 +192,12 @@ public static partial class PoolManager
return (client, logHandler);
}
- private static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
- {
- var cfg = instance.ResolveDependency();
- await instance.WaitPost(() =>
- {
- if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
- cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
-
- if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
- cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
-
- if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
- cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
-
- if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
- cfg.SetCVar(CCVars.GameMap, settings.Map);
-
- if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
- cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
-
- if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
- cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
- });
- }
-
- private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
- {
- foreach (var (cvar, value) in TestCvars)
- {
- options.CVarOverrides[cvar] = value;
- }
- }
-
///
- /// Gets a , which can be used to get access to a server, and client
+ /// Gets a , which can be used to get access to a server, and client
///
/// See
///
- public static async Task GetServerClient(PoolSettings? poolSettings = null)
+ public static async Task GetServerClient(PoolSettings? poolSettings = null)
{
return await GetServerClientPair(poolSettings ?? new PoolSettings());
}
@@ -273,7 +207,7 @@ public static partial class PoolManager
return testContext.Test.FullName.Replace("Content.IntegrationTests.Tests.", "");
}
- private static async Task GetServerClientPair(PoolSettings poolSettings)
+ private static async Task GetServerClientPair(PoolSettings poolSettings)
{
if (!_initialized)
throw new InvalidOperationException($"Pool manager has not been initialized");
@@ -286,7 +220,7 @@ public static partial class PoolManager
var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);
var poolRetrieveTimeWatch = new Stopwatch();
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
- Pair? pair = null;
+ TestPair? pair = null;
try
{
poolRetrieveTimeWatch.Start();
@@ -295,11 +229,6 @@ public static partial class PoolManager
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
pair = await CreateServerClientPair(poolSettings, testOut);
-
- // Newly created pairs should always be in a valid state.
- await RunTicksSync(pair, 5);
- await SyncTicks(pair, targetDelta: 1);
- ValidatePair(pair, poolSettings);
}
else
{
@@ -308,7 +237,6 @@ public static partial class PoolManager
if (pair != null)
{
pair.ActivateContext(testOut);
-
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
@@ -317,17 +245,16 @@ public static partial class PoolManager
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
await SetupCVars(pair.Client, poolSettings);
await SetupCVars(pair.Server, poolSettings);
- await RunTicksSync(pair, 1);
+ await pair.RunTicksSync(1);
}
else
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
- await CleanPooledPair(poolSettings, pair, testOut);
+ await pair.CleanPooledPair(poolSettings, testOut);
}
- await RunTicksSync(pair, 5);
- await SyncTicks(pair, targetDelta: 1);
- ValidatePair(pair, poolSettings);
+ await pair.RunTicksSync(5);
+ await pair.SyncTicks(targetDelta: 1);
}
else
{
@@ -335,114 +262,65 @@ public static partial class PoolManager
pair = await CreateServerClientPair(poolSettings, testOut);
}
}
-
}
finally
{
if (pair != null && pair.TestHistory.Count > 1)
{
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History Start");
+ await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start");
for (var i = 0; i < pair.TestHistory.Count; i++)
{
- await testOut.WriteLineAsync($"- Pair {pair.PairId} Test #{i}: {pair.TestHistory[i]}");
+ await testOut.WriteLineAsync($"- Pair {pair.Id} Test #{i}: {pair.TestHistory[i]}");
}
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History End");
+ await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History End");
}
}
+
+ pair.ValidateSettings(poolSettings);
+
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
await testOut.WriteLineAsync(
- $"{nameof(GetServerClientPair)}: Retrieving pair {pair.PairId} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
+ $"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
await testOut.WriteLineAsync(
- $"{nameof(GetServerClientPair)}: Returning pair {pair.PairId}");
+ $"{nameof(GetServerClientPair)}: Returning pair {pair.Id}");
pair.Settings = poolSettings;
pair.TestHistory.Add(currentTestName);
- var usageWatch = new Stopwatch();
- usageWatch.Start();
-
- return new PairTracker(testOut)
- {
- Pair = pair,
- UsageWatch = usageWatch
- };
+ pair.Watch.Restart();
+ return pair;
}
- private static void ValidatePair(Pair pair, PoolSettings settings)
- {
- var cfg = pair.Server.ResolveDependency();
- Assert.That(cfg.GetCVar(CCVars.AdminLogsEnabled), Is.EqualTo(settings.AdminLogsEnabled));
- Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby));
- Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.UseDummyTicker));
-
- var entMan = pair.Server.ResolveDependency();
- var ticker = entMan.System();
- Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
-
- var expectPreRound = settings.InLobby | settings.DummyTicker;
- var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
- Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
-
- var baseClient = pair.Client.ResolveDependency();
- var netMan = pair.Client.ResolveDependency();
- Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
-
- if (!settings.ShouldBeConnected)
- return;
-
- Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
- var cPlayer = pair.Client.ResolveDependency();
- var sPlayer = pair.Server.ResolveDependency();
- Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
- var session = sPlayer.Sessions.Single();
- Assert.That(cPlayer.LocalPlayer?.Session.UserId, Is.EqualTo(session.UserId));
-
- if (ticker.DummyTicker)
- return;
-
- var status = ticker.PlayerGameStatuses[session.UserId];
- var expected = settings.InLobby
- ? PlayerGameStatus.NotReadyToPlay
- : PlayerGameStatus.JoinedGame;
-
- Assert.That(status, Is.EqualTo(expected));
-
- if (settings.InLobby)
- {
- Assert.Null(session.AttachedEntity);
- return;
- }
-
- Assert.NotNull(session.AttachedEntity);
- Assert.That(entMan.EntityExists(session.AttachedEntity));
- Assert.That(entMan.HasComponent(session.AttachedEntity));
- var mindCont = entMan.GetComponent(session.AttachedEntity!.Value);
- Assert.NotNull(mindCont.Mind);
- Assert.Null(mindCont.Mind?.VisitingEntity);
- Assert.That(mindCont.Mind!.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
- Assert.That(mindCont.Mind.UserId, Is.EqualTo(session.UserId));
- }
-
- private static Pair? GrabOptimalPair(PoolSettings poolSettings)
+ private static TestPair? GrabOptimalPair(PoolSettings poolSettings)
{
lock (PairLock)
{
- Pair? fallback = null;
+ TestPair? fallback = null;
foreach (var pair in Pairs.Keys)
{
if (Pairs[pair])
continue;
+
if (!pair.Settings.CanFastRecycle(poolSettings))
{
fallback = pair;
continue;
}
+
+ pair.Use();
Pairs[pair] = true;
return pair;
}
if (fallback != null)
{
+ fallback.Use();
Pairs[fallback!] = true;
}
+
+ if (fallback == null && _pairId > 8)
+ {
+ var x = 2;
+ }
+
return fallback;
}
}
@@ -451,78 +329,19 @@ public static partial class PoolManager
/// Used by PairTracker after checking the server/client pair, Don't use this.
///
///
- public static void NoCheckReturn(Pair pair)
+ public static void NoCheckReturn(TestPair pair)
{
lock (PairLock)
{
- if (pair.Dead)
- {
+ if (pair.State == TestPair.PairState.Dead)
Pairs.Remove(pair);
- }
- else
- {
+ else if (pair.State == TestPair.PairState.Ready)
Pairs[pair] = false;
- }
+ else
+ throw new InvalidOperationException($"Attempted to return a pair in an invalid state. Pair: {pair.Id}. State: {pair.State}.");
}
}
- private static async Task CleanPooledPair(PoolSettings settings, Pair pair, TextWriter testOut)
- {
- pair.Settings = default!;
- var methodWatch = new Stopwatch();
- methodWatch.Start();
- await testOut.WriteLineAsync($"Recycling...");
-
- var configManager = pair.Server.ResolveDependency();
- var entityManager = pair.Server.ResolveDependency();
- var gameTicker = entityManager.System();
- var cNetMgr = pair.Client.ResolveDependency();
-
- await RunTicksSync(pair, 1);
-
- // Disconnect the client if they are connected.
- if (cNetMgr.IsConnected)
- {
- await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
- await pair.Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
- await RunTicksSync(pair, 1);
- }
- Assert.That(cNetMgr.IsConnected, Is.False);
-
- // Move to pre-round lobby. Required to toggle dummy ticker on and off
- if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
- {
- await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Restarting server.");
- Assert.That(gameTicker.DummyTicker, Is.False);
- configManager.SetCVar(CCVars.GameLobbyEnabled, true);
- await pair.Server.WaitPost(() => gameTicker.RestartRound());
- await RunTicksSync(pair, 1);
- }
-
- //Apply Cvars
- await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
- await SetupCVars(pair.Client, settings);
- await SetupCVars(pair.Server, settings);
- await RunTicksSync(pair, 1);
-
- // Restart server.
- await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Restarting server again");
- await pair.Server.WaitPost(() => gameTicker.RestartRound());
- await RunTicksSync(pair, 1);
-
- // Connect client
- if (settings.ShouldBeConnected)
- {
- await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Connecting client");
- pair.Client.SetConnectTarget(pair.Server);
- await pair.Client.WaitPost(() => cNetMgr.ClientConnect(null!, 0, null!));
- }
-
- await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Idling");
- await ReallyBeIdle(pair);
- await testOut.WriteLineAsync($"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Done recycling");
- }
-
private static void DieIfPoolFailure()
{
if (_poolFailureReason != null)
@@ -543,52 +362,23 @@ we are just going to end this here to save a lot of time. This is the exception
}
}
- private static async Task CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
+ private static async Task CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
{
- Pair pair;
try
{
- var (client, clientLog) = await GenerateClient(poolSettings, testOut);
- var (server, serverLog) = await GenerateServer(poolSettings, testOut);
- pair = new Pair
- {
- Server = server,
- ServerLogHandler = serverLog,
- Client = client,
- ClientLogHandler = clientLog,
- PairId = Interlocked.Increment(ref _pairId)
- };
-
- if (!poolSettings.NoLoadTestPrototypes)
- await pair.LoadPrototypes(_testPrototypes!);
+ var id = Interlocked.Increment(ref _pairId);
+ var pair = new TestPair(id);
+ await pair.Initialize(poolSettings, testOut, _testPrototypes);
+ pair.Use();
+ await pair.RunTicksSync(5);
+ await pair.SyncTicks(targetDelta: 1);
+ return pair;
}
catch (Exception ex)
{
_poolFailureReason = ex;
throw;
}
-
- if (!poolSettings.UseDummyTicker)
- {
- var gameTicker = pair.Server.ResolveDependency().System();
- await pair.Server.WaitPost(() => gameTicker.RestartRound());
- }
-
- if (poolSettings.ShouldBeConnected)
- {
- pair.Client.SetConnectTarget(pair.Server);
- await pair.Client.WaitPost(() =>
- {
- var netMgr = IoCManager.Resolve();
- if (!netMgr.IsConnected)
- {
- netMgr.ClientConnect(null!, 0, null!);
- }
- });
- await ReallyBeIdle(pair, 10);
- await pair.Client.WaitRunTicks(1);
- }
- return pair;
}
///
@@ -596,36 +386,10 @@ we are just going to end this here to save a lot of time. This is the exception
///
/// A pairTracker
/// A TestMapData
- public static async Task CreateTestMap(PairTracker pairTracker)
+ [Obsolete("use TestPair.CreateMap")]
+ public static async Task CreateTestMap(TestPair pairTracker)
{
- var server = pairTracker.Pair.Server;
-
- await server.WaitIdleAsync();
-
- var settings = pairTracker.Pair.Settings;
- var mapManager = server.ResolveDependency();
- var tileDefinitionManager = server.ResolveDependency();
-
- var mapData = new TestMapData();
- await server.WaitPost(() =>
- {
- mapData.MapId = mapManager.CreateMap();
- mapData.MapUid = mapManager.GetMapEntityId(mapData.MapId);
- mapData.MapGrid = mapManager.CreateGrid(mapData.MapId);
- mapData.GridUid = mapData.MapGrid.Owner; // Fixing this requires an engine PR.
- mapData.GridCoords = new EntityCoordinates(mapData.GridUid, 0, 0);
- var plating = tileDefinitionManager["Plating"];
- var platingTile = new Tile(plating.TileId);
- mapData.MapGrid.SetTile(mapData.GridCoords, platingTile);
- mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
- mapData.Tile = mapData.MapGrid.GetAllTiles().First();
- });
- if (settings.ShouldBeConnected)
- {
- await RunTicksSync(pairTracker.Pair, 10);
- }
-
- return mapData;
+ return await pairTracker.CreateTestMap();
}
///
@@ -633,7 +397,8 @@ we are just going to end this here to save a lot of time. This is the exception
///
/// A server/client pair
/// How many ticks to run them for
- public static async Task RunTicksSync(Pair pair, int ticks)
+ [Obsolete("use TestPair.RunTicks")]
+ public static async Task RunTicksSync(TestPair pair, int ticks)
{
for (var i = 0; i < ticks; i++)
{
@@ -641,51 +406,7 @@ we are just going to end this here to save a lot of time. This is the exception
await pair.Client.WaitRunTicks(1);
}
}
-
- ///
- /// Runs the server/client in sync, but also ensures they are both idle each tick.
- ///
- /// The server/client pair
- /// How many ticks to run
- public static async Task ReallyBeIdle(Pair pair, int runTicks = 25)
- {
- for (var i = 0; i < runTicks; i++)
- {
- await pair.Client.WaitRunTicks(1);
- await pair.Server.WaitRunTicks(1);
- for (var idleCycles = 0; idleCycles < 4; idleCycles++)
- {
- await pair.Client.WaitIdleAsync();
- await pair.Server.WaitIdleAsync();
- }
- }
- }
-
- ///
- /// Run the server/clients until the ticks are synchronized.
- /// By default the client will be one tick ahead of the server.
- ///
- public static async Task SyncTicks(Pair pair, int targetDelta = 1)
- {
- var sTiming = pair.Server.ResolveDependency();
- var cTiming = pair.Client.ResolveDependency();
- var sTick = (int)sTiming.CurTick.Value;
- var cTick = (int)cTiming.CurTick.Value;
- var delta = cTick - sTick;
-
- if (delta == targetDelta)
- return;
- if (delta > targetDelta)
- await pair.Server.WaitRunTicks(delta - targetDelta);
- else
- await pair.Client.WaitRunTicks(targetDelta - delta);
-
- sTick = (int)sTiming.CurTick.Value;
- cTick = (int)cTiming.CurTick.Value;
- delta = cTick - sTick;
- Assert.That(delta, Is.EqualTo(targetDelta));
- }
-
+
///
/// Runs a server, or a client until a condition is true
///
@@ -743,7 +464,7 @@ we are just going to end this here to save a lot of time. This is the exception
///
/// Helper method that retrieves all entity prototypes that have some component.
///
- public static List GetEntityPrototypes(RobustIntegrationTest.IntegrationInstance instance) where T : Component
+ public static List GetPrototypesWithComponent(RobustIntegrationTest.IntegrationInstance instance) where T : Component
{
var protoMan = instance.ResolveDependency();
var compFact = instance.ResolveDependency();
@@ -771,339 +492,4 @@ we are just going to end this here to save a lot of time. This is the exception
_initialized = true;
DiscoverTestPrototypes(assembly);
}
-}
-
-///
-/// Settings for the pooled server, and client pair.
-/// Some options are for changing the pair, and others are
-/// so the pool can properly clean up what you borrowed.
-///
-public sealed class PoolSettings
-{
- ///
- /// If the returned pair must not be reused
- ///
- public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
-
- ///
- /// If the given pair must be brand new
- ///
- public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
-
- ///
- /// Set to true if the test will ruin the server/client pair.
- ///
- public bool Destructive { get; init; }
-
- ///
- /// Set to true if the given server/client pair should be created fresh.
- ///
- public bool Fresh { get; init; }
-
- ///
- /// Set to true if the given server should be using a dummy ticker. Ignored if is true.
- ///
- public bool DummyTicker { get; init; } = true;
-
- public bool UseDummyTicker => !InLobby && DummyTicker;
-
- ///
- /// If true, this enables the creation of admin logs during the test.
- ///
- public bool AdminLogsEnabled { get; init; }
-
- ///
- /// Set to true if the given server/client pair should be connected from each other.
- /// Defaults to disconnected as it makes dirty recycling slightly faster.
- /// If is true, this option is ignored.
- ///
- public bool Connected { get; init; }
-
- public bool ShouldBeConnected => InLobby || Connected;
-
- ///
- /// Set to true if the given server/client pair should be in the lobby.
- /// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
- ///
- ///
- /// If this is enabled, the value of is ignored.
- ///
- public bool InLobby { get; init; }
-
- ///
- /// Set this to true to skip loading the content files.
- /// Note: This setting won't work with a client.
- ///
- public bool NoLoadContent { get; init; }
-
- ///
- /// This will return a server-client pair that has not loaded test prototypes.
- /// Try avoiding this whenever possible, as this will always create & destroy a new pair.
- /// Use if you need to exclude test prototypees.
- ///
- public bool NoLoadTestPrototypes { get; init; }
-
- ///
- /// Set this to true to disable the NetInterp CVar on the given server/client pair
- ///
- public bool DisableInterpolate { get; init; }
-
- ///
- /// Set this to true to always clean up the server/client pair before giving it to another borrower
- ///
- public bool Dirty { get; init; }
-
- ///
- /// Set this to the path of a map to have the given server/client pair load the map.
- ///
- public string Map { get; init; } = PoolManager.TestMap;
-
- ///
- /// Overrides the test name detection, and uses this in the test history instead
- ///
- public string? TestName { get; set; }
-
- ///
- /// Tries to guess if we can skip recycling the server/client pair.
- ///
- /// The next set of settings the old pair will be set to
- /// If we can skip cleaning it up
- public bool CanFastRecycle(PoolSettings nextSettings)
- {
- if (MustNotBeReused)
- throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
-
- if (nextSettings.MustBeNew)
- throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
-
- if (Dirty)
- return false;
-
- // Check that certain settings match.
- return !ShouldBeConnected == !nextSettings.ShouldBeConnected
- && UseDummyTicker == nextSettings.UseDummyTicker
- && Map == nextSettings.Map
- && InLobby == nextSettings.InLobby;
- }
-}
-
-///
-/// Holds a reference to things commonly needed when testing on a map
-///
-public sealed class TestMapData
-{
- public EntityUid MapUid { get; set; }
- public EntityUid GridUid { get; set; }
- public MapId MapId { get; set; }
- public MapGridComponent MapGrid { get; set; } = default!;
- public EntityCoordinates GridCoords { get; set; }
- public MapCoordinates MapCoords { get; set; }
- public TileRef Tile { get; set; }
-}
-
-///
-/// A server/client pair
-///
-public sealed class Pair
-{
- public bool Dead { get; private set; }
- public int PairId { get; init; }
- public List TestHistory { get; set; } = new();
- public PoolSettings Settings { get; set; } = default!;
- public RobustIntegrationTest.ServerIntegrationInstance Server { get; init; } = default!;
- public RobustIntegrationTest.ClientIntegrationInstance Client { get; init; } = default!;
-
- public PoolTestLogHandler ServerLogHandler { get; init; } = default!;
- public PoolTestLogHandler ClientLogHandler { get; init; } = default!;
-
- private Dictionary> _loadedPrototypes = new();
- private HashSet _loadedEntityPrototypes = new();
-
- public void Kill()
- {
- Dead = true;
- Server.Dispose();
- Client.Dispose();
- }
-
- public void ClearContext()
- {
- ServerLogHandler.ClearContext();
- ClientLogHandler.ClearContext();
- }
-
- public void ActivateContext(TextWriter testOut)
- {
- ServerLogHandler.ActivateContext(testOut);
- ClientLogHandler.ActivateContext(testOut);
- }
-
- public async Task LoadPrototypes(List prototypes)
- {
- await LoadPrototypes(Server, prototypes);
- await LoadPrototypes(Client, prototypes);
- }
-
- private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List prototypes)
- {
- var changed = new Dictionary>();
- var protoMan = instance.ResolveDependency();
- foreach (var file in prototypes)
- {
- protoMan.LoadString(file, changed: changed);
- }
-
- await instance.WaitPost(() => protoMan.ReloadPrototypes(changed));
-
- foreach (var (kind, ids) in changed)
- {
- _loadedPrototypes.GetOrNew(kind).UnionWith(ids);
- }
-
- if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
- _loadedEntityPrototypes.UnionWith(entIds);
- }
-
- public bool IsTestPrototype(EntityPrototype proto)
- {
- return _loadedEntityPrototypes.Contains(proto.ID);
- }
-
- public bool IsTestEntityPrototype(string id)
- {
- return _loadedEntityPrototypes.Contains(id);
- }
-
- public bool IsTestPrototype(string id) where TPrototype : IPrototype
- {
- return IsTestPrototype(typeof(TPrototype), id);
- }
-
- public bool IsTestPrototype(TPrototype proto) where TPrototype : IPrototype
- {
- return IsTestPrototype(typeof(TPrototype), proto.ID);
- }
-
- public bool IsTestPrototype(Type kind, string id)
- {
- return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
- }
-}
-
-///
-/// Used by the pool to keep track of a borrowed server/client pair.
-///
-public sealed class PairTracker : IAsyncDisposable
-{
- private readonly TextWriter _testOut;
- private int _disposed;
- public Stopwatch UsageWatch { get; set; } = default!;
- public Pair Pair { get; init; } = default!;
-
- public PairTracker(TextWriter testOut)
- {
- _testOut = testOut;
- }
-
- // Convenience properties.
- public RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server;
- public RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client;
-
- private async Task OnDirtyDispose()
- {
- var usageTime = UsageWatch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Pair.PairId} in {usageTime.TotalMilliseconds} ms");
- var dirtyWatch = new Stopwatch();
- dirtyWatch.Start();
- Pair.Kill();
- PoolManager.NoCheckReturn(Pair);
- var disposeTime = dirtyWatch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Pair.PairId} in {disposeTime.TotalMilliseconds} ms");
-
- // Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
- // because someone forgot to clean-return the pair.
- Assert.Warn("Test was dirty-disposed.");
- }
-
- private async Task OnCleanDispose()
- {
- var usageTime = UsageWatch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Pair.PairId} for {usageTime.TotalMilliseconds} ms");
- var cleanWatch = new Stopwatch();
- cleanWatch.Start();
- // Let any last minute failures the test cause happen.
- await PoolManager.ReallyBeIdle(Pair);
- if (!Pair.Settings.Destructive)
- {
- if (Pair.Client.IsAlive == false)
- {
- throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Pair.PairId}:", Pair.Client.UnhandledException);
- }
-
- if (Pair.Server.IsAlive == false)
- {
- throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Pair.PairId}:", Pair.Server.UnhandledException);
- }
- }
-
- if (Pair.Settings.MustNotBeReused)
- {
- Pair.Kill();
- PoolManager.NoCheckReturn(Pair);
- await PoolManager.ReallyBeIdle(Pair);
- var returnTime2 = cleanWatch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {returnTime2.TotalMilliseconds} ms");
- return;
- }
-
- var sRuntimeLog = Pair.Server.ResolveDependency();
- if (sRuntimeLog.ExceptionCount > 0)
- throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
- var cRuntimeLog = Pair.Client.ResolveDependency();
- if (cRuntimeLog.ExceptionCount > 0)
- throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
-
- Pair.ClearContext();
- PoolManager.NoCheckReturn(Pair);
- var returnTime = cleanWatch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Pair.PairId} back into the pool");
- }
-
- public async ValueTask CleanReturnAsync()
- {
- var disposed = Interlocked.Exchange(ref _disposed, 1);
- switch (disposed)
- {
- case 0:
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Pair.PairId} started");
- break;
- case 1:
- throw new Exception($"{nameof(CleanReturnAsync)}: Already clean returned");
- case 2:
- throw new Exception($"{nameof(CleanReturnAsync)}: Already dirty disposed");
- default:
- throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected disposed value");
- }
-
- await OnCleanDispose();
- }
-
- public async ValueTask DisposeAsync()
- {
- var disposed = Interlocked.Exchange(ref _disposed, 2);
- switch (disposed)
- {
- case 0:
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Pair.PairId} started");
- break;
- case 1:
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Pair {Pair.PairId} was properly clean disposed");
- return;
- case 2:
- throw new Exception($"{nameof(DisposeAsync)}: Already dirty disposed pair {Pair.PairId}");
- default:
- throw new Exception($"{nameof(DisposeAsync)}: Unexpected disposed value");
- }
- await OnDirtyDispose();
- }
-}
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/PoolSettings.cs b/Content.IntegrationTests/PoolSettings.cs
new file mode 100644
index 0000000000..a78173808f
--- /dev/null
+++ b/Content.IntegrationTests/PoolSettings.cs
@@ -0,0 +1,117 @@
+#nullable enable
+
+namespace Content.IntegrationTests;
+
+///
+/// Settings for the pooled server, and client pair.
+/// Some options are for changing the pair, and others are
+/// so the pool can properly clean up what you borrowed.
+///
+public sealed class PoolSettings
+{
+ ///
+ /// If the returned pair must not be reused
+ ///
+ public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
+
+ ///
+ /// If the given pair must be brand new
+ ///
+ public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
+
+ ///
+ /// Set to true if the test will ruin the server/client pair.
+ ///
+ public bool Destructive { get; init; }
+
+ ///
+ /// Set to true if the given server/client pair should be created fresh.
+ ///
+ public bool Fresh { get; init; }
+
+ ///
+ /// Set to true if the given server should be using a dummy ticker. Ignored if is true.
+ ///
+ public bool DummyTicker { get; init; } = true;
+
+ public bool UseDummyTicker => !InLobby && DummyTicker;
+
+ ///
+ /// If true, this enables the creation of admin logs during the test.
+ ///
+ public bool AdminLogsEnabled { get; init; }
+
+ ///
+ /// Set to true if the given server/client pair should be connected from each other.
+ /// Defaults to disconnected as it makes dirty recycling slightly faster.
+ /// If is true, this option is ignored.
+ ///
+ public bool Connected { get; init; }
+
+ public bool ShouldBeConnected => InLobby || Connected;
+
+ ///
+ /// Set to true if the given server/client pair should be in the lobby.
+ /// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
+ ///
+ ///
+ /// If this is enabled, the value of is ignored.
+ ///
+ public bool InLobby { get; init; }
+
+ ///
+ /// Set this to true to skip loading the content files.
+ /// Note: This setting won't work with a client.
+ ///
+ public bool NoLoadContent { get; init; }
+
+ ///
+ /// This will return a server-client pair that has not loaded test prototypes.
+ /// Try avoiding this whenever possible, as this will always create & destroy a new pair.
+ /// Use if you need to exclude test prototypees.
+ ///
+ public bool NoLoadTestPrototypes { get; init; }
+
+ ///
+ /// Set this to true to disable the NetInterp CVar on the given server/client pair
+ ///
+ public bool DisableInterpolate { get; init; }
+
+ ///
+ /// Set this to true to always clean up the server/client pair before giving it to another borrower
+ ///
+ public bool Dirty { get; init; }
+
+ ///
+ /// Set this to the path of a map to have the given server/client pair load the map.
+ ///
+ public string Map { get; init; } = PoolManager.TestMap;
+
+ ///
+ /// Overrides the test name detection, and uses this in the test history instead
+ ///
+ public string? TestName { get; set; }
+
+ ///
+ /// Tries to guess if we can skip recycling the server/client pair.
+ ///
+ /// The next set of settings the old pair will be set to
+ /// If we can skip cleaning it up
+ public bool CanFastRecycle(PoolSettings nextSettings)
+ {
+ if (MustNotBeReused)
+ throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
+
+ if (nextSettings.MustBeNew)
+ throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
+
+ if (Dirty)
+ return false;
+
+ // Check that certain settings match.
+ return !ShouldBeConnected == !nextSettings.ShouldBeConnected
+ && UseDummyTicker == nextSettings.UseDummyTicker
+ && Map == nextSettings.Map
+ && InLobby == nextSettings.InLobby;
+ }
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs
index a6788b0ff7..a24985d738 100644
--- a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs
+++ b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs
@@ -146,7 +146,7 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(0));
client.SetConnectTarget(server);
await client.WaitPost(() => netMan.ClientConnect(null!, 0, null!));
- await PoolManager.ReallyBeIdle(pairTracker.Pair);
+ await pairTracker.RunTicksSync(5);
Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(1));
await pairTracker.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index df1563419f..f59c7a2cfa 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Numerics;
using Content.Client.Construction;
using Content.Client.Examine;
+using Content.IntegrationTests.Pair;
using Content.Server.Body.Systems;
using Content.Server.Mind;
using Content.Server.Players;
@@ -40,11 +41,11 @@ public abstract partial class InteractionTest
{
protected virtual string PlayerPrototype => "InteractionTestMob";
- protected PairTracker PairTracker = default!;
- protected TestMapData MapData = default!;
+ protected TestPair PairTracker = default!;
+ protected TestMapData MapData => PairTracker.TestMap!;
- protected RobustIntegrationTest.ServerIntegrationInstance Server => PairTracker.Pair.Server;
- protected RobustIntegrationTest.ClientIntegrationInstance Client => PairTracker.Pair.Client;
+ protected RobustIntegrationTest.ServerIntegrationInstance Server => PairTracker.Server;
+ protected RobustIntegrationTest.ClientIntegrationInstance Client => PairTracker.Client;
protected MapId MapId => MapData.MapId;
@@ -172,7 +173,7 @@ public abstract partial class InteractionTest
CLogger = Client.ResolveDependency().RootSawmill;
// Setup map.
- MapData = await PoolManager.CreateTestMap(PairTracker);
+ await PairTracker.CreateTestMap();
PlayerCoords = MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan);
TargetCoords = MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan);
await SetTile(Plating, grid: MapData.MapGrid);
@@ -225,7 +226,7 @@ public abstract partial class InteractionTest
});
// Final player asserts/checks.
- await PoolManager.ReallyBeIdle(PairTracker.Pair, 5);
+ await PairTracker.ReallyBeIdle(5);
Assert.Multiple(() =>
{
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(Player));
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
index 152715d471..d984b31b0e 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.IntegrationTests.Pair;
using Content.Server.Ghost.Components;
using Content.Server.Mind;
using Content.Server.Players;
@@ -23,7 +24,7 @@ public sealed partial class MindTests
/// the player's mind's current entity, likely because some previous test directly changed the players attached
/// entity.
///
- private static async Task SetupPair(bool dirty = false)
+ private static async Task SetupPair(bool dirty = false)
{
var pairTracker = await PoolManager.GetServerClient(new PoolSettings
{
@@ -61,7 +62,7 @@ public sealed partial class MindTests
return pairTracker;
}
- private static async Task BecomeGhost(Pair pair, bool visit = false)
+ private static async Task BecomeGhost(TestPair pair, bool visit = false)
{
var entMan = pair.Server.ResolveDependency();
var playerMan = pair.Server.ResolveDependency();
@@ -103,7 +104,7 @@ public sealed partial class MindTests
return ghostUid;
}
- private static async Task VisitGhost(Pair pair, bool _ = false)
+ private static async Task VisitGhost(Pair.TestPair pair, bool _ = false)
{
return await BecomeGhost(pair, visit: true);
}
@@ -111,7 +112,7 @@ public sealed partial class MindTests
///
/// Get the player's current mind and check that the entities exists.
///
- private static Mind GetMind(Pair pair)
+ private static Mind GetMind(Pair.TestPair pair)
{
var playerMan = pair.Server.ResolveDependency();
var entMan = pair.Server.ResolveDependency();
@@ -130,7 +131,7 @@ public sealed partial class MindTests
return mind;
}
- private static async Task Disconnect(Pair pair)
+ private static async Task Disconnect(Pair.TestPair pair)
{
var netManager = pair.Client.ResolveDependency();
var playerMan = pair.Server.ResolveDependency();
@@ -151,7 +152,7 @@ public sealed partial class MindTests
});
}
- private static async Task Connect(Pair pair, string username)
+ private static async Task Connect(Pair.TestPair pair, string username)
{
var netManager = pair.Client.ResolveDependency();
var playerMan = pair.Server.ResolveDependency();
@@ -166,7 +167,7 @@ public sealed partial class MindTests
Assert.That(player.Status, Is.EqualTo(SessionStatus.InGame));
}
- private static async Task DisconnectReconnect(Pair pair)
+ private static async Task DisconnectReconnect(Pair.TestPair pair)
{
var playerMan = pair.Server.ResolveDependency();
var player = playerMan.ServerSessions.Single();
diff --git a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
index 8533c1aad0..2751e46026 100644
--- a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
+++ b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
@@ -60,8 +60,8 @@ namespace Content.IntegrationTests.Tests.Networking
});
// Run some ticks and ensure that the buffer has filled up.
- await PoolManager.SyncTicks(pairTracker.Pair);
- await PoolManager.RunTicksSync(pairTracker.Pair, 25);
+ await pairTracker.SyncTicks();
+ await pairTracker.RunTicksSync(25);
Assert.That(cGameTiming.TickTimingAdjustment, Is.EqualTo(0));
Assert.That(sGameTiming.TickTimingAdjustment, Is.EqualTo(0));
diff --git a/Content.IntegrationTests/Tests/RoundEndTest.cs b/Content.IntegrationTests/Tests/RoundEndTest.cs
index c307916841..0a6bf20153 100644
--- a/Content.IntegrationTests/Tests/RoundEndTest.cs
+++ b/Content.IntegrationTests/Tests/RoundEndTest.cs
@@ -137,8 +137,6 @@ namespace Content.IntegrationTests.Tests
roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMinutes(4);
ticker.RestartRound();
});
- await PoolManager.ReallyBeIdle(pairTracker.Pair, 10);
-
await pairTracker.CleanReturnAsync();
}
}
diff --git a/Content.IntegrationTests/Tests/StackTest.cs b/Content.IntegrationTests/Tests/StackTest.cs
index 54688f8ea4..9eeef87346 100644
--- a/Content.IntegrationTests/Tests/StackTest.cs
+++ b/Content.IntegrationTests/Tests/StackTest.cs
@@ -19,7 +19,7 @@ public sealed class StackTest
Assert.Multiple(() =>
{
- foreach (var entity in PoolManager.GetEntityPrototypes(server))
+ foreach (var entity in PoolManager.GetPrototypesWithComponent(server))
{
if (!entity.TryGetComponent(out var stackComponent, compFact) ||
!entity.TryGetComponent(out var itemComponent, compFact))
diff --git a/Content.IntegrationTests/Tests/StorageTest.cs b/Content.IntegrationTests/Tests/StorageTest.cs
index f63bb9c399..61edd4eaa3 100644
--- a/Content.IntegrationTests/Tests/StorageTest.cs
+++ b/Content.IntegrationTests/Tests/StorageTest.cs
@@ -79,7 +79,7 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
- foreach (var proto in PoolManager.GetEntityPrototypes(server))
+ foreach (var proto in PoolManager.GetPrototypesWithComponent(server))
{
int capacity;
var isEntStorage = false;
diff --git a/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs
index d04c6c6b7e..049c83084f 100644
--- a/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs
+++ b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs
@@ -1,5 +1,6 @@
#nullable enable
using System.Collections.Generic;
+using Content.IntegrationTests.Pair;
using Content.Server.Administration.Managers;
using Robust.Server.Player;
using Robust.Shared.Players;
@@ -14,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Toolshed;
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract class ToolshedTest : IInvocationContext
{
- protected PairTracker PairTracker = default!;
+ protected TestPair PairTracker = default!;
protected virtual bool Connected => false;
protected virtual bool AssertOnUnexpectedError => true;