Split PoolManager into separate classes. (#19370)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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!;
|
||||
|
||||
|
||||
19
Content.IntegrationTests/Pair/TestMapData.cs
Normal file
19
Content.IntegrationTests/Pair/TestMapData.cs
Normal 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; }
|
||||
}
|
||||
39
Content.IntegrationTests/Pair/TestPair.Helpers.cs
Normal file
39
Content.IntegrationTests/Pair/TestPair.Helpers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
64
Content.IntegrationTests/Pair/TestPair.Prototypes.cs
Normal file
64
Content.IntegrationTests/Pair/TestPair.Prototypes.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
218
Content.IntegrationTests/Pair/TestPair.Recycle.cs
Normal file
218
Content.IntegrationTests/Pair/TestPair.Recycle.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
62
Content.IntegrationTests/Pair/TestPair.Timing.cs
Normal file
62
Content.IntegrationTests/Pair/TestPair.Timing.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
111
Content.IntegrationTests/Pair/TestPair.cs
Normal file
111
Content.IntegrationTests/Pair/TestPair.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
70
Content.IntegrationTests/PoolManager.Cvars.cs
Normal file
70
Content.IntegrationTests/PoolManager.Cvars.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
117
Content.IntegrationTests/PoolSettings.cs
Normal file
117
Content.IntegrationTests/PoolSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -137,8 +137,6 @@ namespace Content.IntegrationTests.Tests
|
||||
roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMinutes(4);
|
||||
ticker.RestartRound();
|
||||
});
|
||||
await PoolManager.ReallyBeIdle(pairTracker.Pair, 10);
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user