Split PoolManager into separate classes. (#19370)

This commit is contained in:
Leon Friedrich
2023-08-23 00:14:01 +12:00
committed by GitHub
parent efdb756065
commit 89a287c1fd
19 changed files with 785 additions and 696 deletions

View File

@@ -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;

View File

@@ -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!;

View File

@@ -0,0 +1,19 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.IntegrationTests.Pair;
/// <summary>
/// Simple data class that stored information about a map being used by a test.
/// </summary>
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; }
}

View File

@@ -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
{
/// <summary>
/// Creates a map, a grid, and a tile, and gives back references to them.
/// </summary>
public async Task<TestMapData> CreateTestMap()
{
await Server.WaitIdleAsync();
var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
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;
}
}

View File

@@ -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<Type, HashSet<string>> _loadedPrototypes = new();
private HashSet<string> _loadedEntityPrototypes = new();
public async Task LoadPrototypes(List<string> prototypes)
{
await LoadPrototypes(Server, prototypes);
await LoadPrototypes(Client, prototypes);
}
private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List<string> prototypes)
{
var changed = new Dictionary<Type, HashSet<string>>();
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<TPrototype>(string id) where TPrototype : IPrototype
{
return IsTestPrototype(typeof(TPrototype), id);
}
public bool IsTestPrototype<TPrototype>(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);
}
}

View File

@@ -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<IRuntimeLog>();
if (sRuntimeLog.ExceptionCount > 0)
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
var cRuntimeLog = Client.ResolveDependency<IRuntimeLog>();
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<GameTicker>();
var cNetMgr = Client.ResolveDependency<IClientNetManager>();
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<EntityManager>();
var ticker = entMan.System<GameTicker>();
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<IBaseClient>();
var netMan = Client.ResolveDependency<INetManager>();
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<Robust.Client.Player.IPlayerManager>();
var sPlayer = Server.ResolveDependency<IPlayerManager>();
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<MindContainerComponent>(session.AttachedEntity));
var mindCont = entMan.GetComponent<MindContainerComponent>(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));
}
}

View File

@@ -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
{
/// <summary>
/// Runs the server-client pair in sync
/// </summary>
/// <param name="ticks">How many ticks to run them for</param>
public async Task RunTicksSync(int ticks)
{
for (var i = 0; i < ticks; i++)
{
await Server.WaitRunTicks(1);
await Client.WaitRunTicks(1);
}
}
/// <summary>
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
/// </summary>
/// <param name="runTicks">How many ticks to run</param>
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();
}
}
}
/// <summary>
/// Run the server/clients until the ticks are synchronized.
/// By default the client will be one tick ahead of the server.
/// </summary>
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));
}
}

View File

@@ -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;
/// <summary>
/// This object wraps a pooled server+client pair.
/// </summary>
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<string> 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<string> 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<IEntityManager>().System<GameTicker>();
await Server.WaitPost(() => gameTicker.RestartRound());
}
if (settings.ShouldBeConnected)
{
Client.SetConnectTarget(Server);
await Client.WaitPost(() =>
{
var netMgr = IoCManager.Resolve<IClientNetManager>();
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,
}
}

View File

@@ -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<IConfigurationManager>();
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;
}
}
}

View File

@@ -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<Pair, bool> Pairs = new();
private static readonly Dictionary<TestPair, bool> 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
/// </summary>
public static void Shutdown()
{
List<Pair> localPairs;
List<TestPair> 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<IConfigurationManager>();
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;
}
}
/// <summary>
/// Gets a <see cref="PairTracker"/>, which can be used to get access to a server, and client <see cref="Pair"/>
/// Gets a <see cref="Pair.TestPair"/>, which can be used to get access to a server, and client <see cref="Pair.TestPair"/>
/// </summary>
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
/// <returns></returns>
public static async Task<PairTracker> GetServerClient(PoolSettings? poolSettings = null)
public static async Task<TestPair> 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<PairTracker> GetServerClientPair(PoolSettings poolSettings)
private static async Task<TestPair> 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<IConfigurationManager>();
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<EntityManager>();
var ticker = entMan.System<GameTicker>();
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<IBaseClient>();
var netMan = pair.Client.ResolveDependency<INetManager>();
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<Robust.Client.Player.IPlayerManager>();
var sPlayer = pair.Server.ResolveDependency<IPlayerManager>();
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<MindContainerComponent>(session.AttachedEntity));
var mindCont = entMan.GetComponent<MindContainerComponent>(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.
/// </summary>
/// <param name="pair"></param>
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<IConfigurationManager>();
var entityManager = pair.Server.ResolveDependency<IEntityManager>();
var gameTicker = entityManager.System<GameTicker>();
var cNetMgr = pair.Client.ResolveDependency<IClientNetManager>();
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<Pair> CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
private static async Task<TestPair> 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<IEntityManager>().System<GameTicker>();
await pair.Server.WaitPost(() => gameTicker.RestartRound());
}
if (poolSettings.ShouldBeConnected)
{
pair.Client.SetConnectTarget(pair.Server);
await pair.Client.WaitPost(() =>
{
var netMgr = IoCManager.Resolve<IClientNetManager>();
if (!netMgr.IsConnected)
{
netMgr.ClientConnect(null!, 0, null!);
}
});
await ReallyBeIdle(pair, 10);
await pair.Client.WaitRunTicks(1);
}
return pair;
}
/// <summary>
@@ -596,36 +386,10 @@ we are just going to end this here to save a lot of time. This is the exception
/// </summary>
/// <param name="pairTracker">A pairTracker</param>
/// <returns>A TestMapData</returns>
public static async Task<TestMapData> CreateTestMap(PairTracker pairTracker)
[Obsolete("use TestPair.CreateMap")]
public static async Task<TestMapData> CreateTestMap(TestPair pairTracker)
{
var server = pairTracker.Pair.Server;
await server.WaitIdleAsync();
var settings = pairTracker.Pair.Settings;
var mapManager = server.ResolveDependency<IMapManager>();
var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
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();
}
/// <summary>
@@ -633,7 +397,8 @@ we are just going to end this here to save a lot of time. This is the exception
/// </summary>
/// <param name="pair">A server/client pair</param>
/// <param name="ticks">How many ticks to run them for</param>
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);
}
}
/// <summary>
/// Runs the server/client in sync, but also ensures they are both idle each tick.
/// </summary>
/// <param name="pair">The server/client pair</param>
/// <param name="runTicks">How many ticks to run</param>
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();
}
}
}
/// <summary>
/// Run the server/clients until the ticks are synchronized.
/// By default the client will be one tick ahead of the server.
/// </summary>
public static async Task SyncTicks(Pair pair, int targetDelta = 1)
{
var sTiming = pair.Server.ResolveDependency<IGameTiming>();
var cTiming = pair.Client.ResolveDependency<IGameTiming>();
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));
}
/// <summary>
/// Runs a server, or a client until a condition is true
/// </summary>
@@ -743,7 +464,7 @@ we are just going to end this here to save a lot of time. This is the exception
/// <summary>
/// Helper method that retrieves all entity prototypes that have some component.
/// </summary>
public static List<EntityPrototype> GetEntityPrototypes<T>(RobustIntegrationTest.IntegrationInstance instance) where T : Component
public static List<EntityPrototype> GetPrototypesWithComponent<T>(RobustIntegrationTest.IntegrationInstance instance) where T : Component
{
var protoMan = instance.ResolveDependency<IPrototypeManager>();
var compFact = instance.ResolveDependency<IComponentFactory>();
@@ -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);
}
}
/// <summary>
/// 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.
/// </summary>
public sealed class PoolSettings
{
/// <summary>
/// If the returned pair must not be reused
/// </summary>
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
/// <summary>
/// If the given pair must be brand new
/// </summary>
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
/// <summary>
/// Set to true if the test will ruin the server/client pair.
/// </summary>
public bool Destructive { get; init; }
/// <summary>
/// Set to true if the given server/client pair should be created fresh.
/// </summary>
public bool Fresh { get; init; }
/// <summary>
/// Set to true if the given server should be using a dummy ticker. Ignored if <see cref="InLobby"/> is true.
/// </summary>
public bool DummyTicker { get; init; } = true;
public bool UseDummyTicker => !InLobby && DummyTicker;
/// <summary>
/// If true, this enables the creation of admin logs during the test.
/// </summary>
public bool AdminLogsEnabled { get; init; }
/// <summary>
/// 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 <see cref="InLobby"/> is true, this option is ignored.
/// </summary>
public bool Connected { get; init; }
public bool ShouldBeConnected => InLobby || Connected;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If this is enabled, the value of <see cref="DummyTicker"/> is ignored.
/// </remarks>
public bool InLobby { get; init; }
/// <summary>
/// Set this to true to skip loading the content files.
/// Note: This setting won't work with a client.
/// </summary>
public bool NoLoadContent { get; init; }
/// <summary>
/// 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 <see cref="Pair.IsTestPrototype(EntityPrototype)"/> if you need to exclude test prototypees.
/// </summary>
public bool NoLoadTestPrototypes { get; init; }
/// <summary>
/// Set this to true to disable the NetInterp CVar on the given server/client pair
/// </summary>
public bool DisableInterpolate { get; init; }
/// <summary>
/// Set this to true to always clean up the server/client pair before giving it to another borrower
/// </summary>
public bool Dirty { get; init; }
/// <summary>
/// Set this to the path of a map to have the given server/client pair load the map.
/// </summary>
public string Map { get; init; } = PoolManager.TestMap;
/// <summary>
/// Overrides the test name detection, and uses this in the test history instead
/// </summary>
public string? TestName { get; set; }
/// <summary>
/// Tries to guess if we can skip recycling the server/client pair.
/// </summary>
/// <param name="nextSettings">The next set of settings the old pair will be set to</param>
/// <returns>If we can skip cleaning it up</returns>
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;
}
}
/// <summary>
/// Holds a reference to things commonly needed when testing on a map
/// </summary>
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; }
}
/// <summary>
/// A server/client pair
/// </summary>
public sealed class Pair
{
public bool Dead { get; private set; }
public int PairId { get; init; }
public List<string> 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<Type, HashSet<string>> _loadedPrototypes = new();
private HashSet<string> _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<string> prototypes)
{
await LoadPrototypes(Server, prototypes);
await LoadPrototypes(Client, prototypes);
}
private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List<string> prototypes)
{
var changed = new Dictionary<Type, HashSet<string>>();
var protoMan = instance.ResolveDependency<IPrototypeManager>();
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<TPrototype>(string id) where TPrototype : IPrototype
{
return IsTestPrototype(typeof(TPrototype), id);
}
public bool IsTestPrototype<TPrototype>(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);
}
}
/// <summary>
/// Used by the pool to keep track of a borrowed server/client pair.
/// </summary>
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<IRuntimeLog>();
if (sRuntimeLog.ExceptionCount > 0)
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
var cRuntimeLog = Pair.Client.ResolveDependency<IRuntimeLog>();
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();
}
}
}

View File

@@ -0,0 +1,117 @@
#nullable enable
namespace Content.IntegrationTests;
/// <summary>
/// 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.
/// </summary>
public sealed class PoolSettings
{
/// <summary>
/// If the returned pair must not be reused
/// </summary>
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
/// <summary>
/// If the given pair must be brand new
/// </summary>
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
/// <summary>
/// Set to true if the test will ruin the server/client pair.
/// </summary>
public bool Destructive { get; init; }
/// <summary>
/// Set to true if the given server/client pair should be created fresh.
/// </summary>
public bool Fresh { get; init; }
/// <summary>
/// Set to true if the given server should be using a dummy ticker. Ignored if <see cref="InLobby"/> is true.
/// </summary>
public bool DummyTicker { get; init; } = true;
public bool UseDummyTicker => !InLobby && DummyTicker;
/// <summary>
/// If true, this enables the creation of admin logs during the test.
/// </summary>
public bool AdminLogsEnabled { get; init; }
/// <summary>
/// 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 <see cref="InLobby"/> is true, this option is ignored.
/// </summary>
public bool Connected { get; init; }
public bool ShouldBeConnected => InLobby || Connected;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If this is enabled, the value of <see cref="DummyTicker"/> is ignored.
/// </remarks>
public bool InLobby { get; init; }
/// <summary>
/// Set this to true to skip loading the content files.
/// Note: This setting won't work with a client.
/// </summary>
public bool NoLoadContent { get; init; }
/// <summary>
/// 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 <see cref="Pair.TestPair.IsTestPrototype(Robust.Shared.Prototypes.EntityPrototype)"/> if you need to exclude test prototypees.
/// </summary>
public bool NoLoadTestPrototypes { get; init; }
/// <summary>
/// Set this to true to disable the NetInterp CVar on the given server/client pair
/// </summary>
public bool DisableInterpolate { get; init; }
/// <summary>
/// Set this to true to always clean up the server/client pair before giving it to another borrower
/// </summary>
public bool Dirty { get; init; }
/// <summary>
/// Set this to the path of a map to have the given server/client pair load the map.
/// </summary>
public string Map { get; init; } = PoolManager.TestMap;
/// <summary>
/// Overrides the test name detection, and uses this in the test history instead
/// </summary>
public string? TestName { get; set; }
/// <summary>
/// Tries to guess if we can skip recycling the server/client pair.
/// </summary>
/// <param name="nextSettings">The next set of settings the old pair will be set to</param>
/// <returns>If we can skip cleaning it up</returns>
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;
}
}

View File

@@ -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();

View File

@@ -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<ILogManager>().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));

View File

@@ -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.
/// </remarks>
private static async Task<PairTracker> SetupPair(bool dirty = false)
private static async Task<Pair.TestPair> 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<EntityUid> BecomeGhost(Pair pair, bool visit = false)
private static async Task<EntityUid> BecomeGhost(TestPair pair, bool visit = false)
{
var entMan = pair.Server.ResolveDependency<IServerEntityManager>();
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
@@ -103,7 +104,7 @@ public sealed partial class MindTests
return ghostUid;
}
private static async Task<EntityUid> VisitGhost(Pair pair, bool _ = false)
private static async Task<EntityUid> VisitGhost(Pair.TestPair pair, bool _ = false)
{
return await BecomeGhost(pair, visit: true);
}
@@ -111,7 +112,7 @@ public sealed partial class MindTests
/// <summary>
/// Get the player's current mind and check that the entities exists.
/// </summary>
private static Mind GetMind(Pair pair)
private static Mind GetMind(Pair.TestPair pair)
{
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
var entMan = pair.Server.ResolveDependency<IEntityManager>();
@@ -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<IClientNetManager>();
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
@@ -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<IClientNetManager>();
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
@@ -166,7 +167,7 @@ public sealed partial class MindTests
Assert.That(player.Status, Is.EqualTo(SessionStatus.InGame));
}
private static async Task<IPlayerSession> DisconnectReconnect(Pair pair)
private static async Task<IPlayerSession> DisconnectReconnect(Pair.TestPair pair)
{
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
var player = playerMan.ServerSessions.Single();

View File

@@ -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));

View File

@@ -137,8 +137,6 @@ namespace Content.IntegrationTests.Tests
roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMinutes(4);
ticker.RestartRound();
});
await PoolManager.ReallyBeIdle(pairTracker.Pair, 10);
await pairTracker.CleanReturnAsync();
}
}

View File

@@ -19,7 +19,7 @@ public sealed class StackTest
Assert.Multiple(() =>
{
foreach (var entity in PoolManager.GetEntityPrototypes<StackComponent>(server))
foreach (var entity in PoolManager.GetPrototypesWithComponent<StackComponent>(server))
{
if (!entity.TryGetComponent<StackComponent>(out var stackComponent, compFact) ||
!entity.TryGetComponent<ItemComponent>(out var itemComponent, compFact))

View File

@@ -79,7 +79,7 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
foreach (var proto in PoolManager.GetEntityPrototypes<StorageFillComponent>(server))
foreach (var proto in PoolManager.GetPrototypesWithComponent<StorageFillComponent>(server))
{
int capacity;
var isEntStorage = false;

View File

@@ -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;