From b6797afe52fbeac57e8d694061026887ade99107 Mon Sep 17 00:00:00 2001
From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Date: Sun, 21 Sep 2025 17:17:43 +1200
Subject: [PATCH] Move TestPair & PoolManager to engine (#36797)
* Move TestPair & PoolManager to engine
* Add to global usings
* A
* Move ITestContextLike to engine
* Readd cvars partial class
* cleanup diff
---
Content.Benchmarks/GlobalUsings.cs | 3 +
.../ExternalTestContext.cs | 12 -
Content.IntegrationTests/GlobalUsings.cs | 1 +
Content.IntegrationTests/ITestContextLike.cs | 13 -
.../NUnitTestContextWrap.cs | 12 -
Content.IntegrationTests/Pair/TestMapData.cs | 23 -
.../Pair/TestPair.Cvars.cs | 69 ---
.../Pair/TestPair.Helpers.cs | 157 +------
.../Pair/TestPair.Prototypes.cs | 64 ---
.../Pair/TestPair.Recycle.cs | 179 +-------
.../Pair/TestPair.Timing.cs | 77 ----
Content.IntegrationTests/Pair/TestPair.cs | 214 ++++-----
Content.IntegrationTests/PoolManager.Cvars.cs | 47 +-
.../PoolManager.Prototypes.cs | 35 --
Content.IntegrationTests/PoolManager.cs | 417 ++----------------
Content.IntegrationTests/PoolSettings.cs | 115 +----
.../PoolTestLogHandler.cs | 79 ----
.../TestPrototypesAttribute.cs | 12 -
Content.MapRenderer/Painters/MapPainter.cs | 1 +
Content.MapRenderer/Program.cs | 1 +
20 files changed, 171 insertions(+), 1360 deletions(-)
create mode 100644 Content.Benchmarks/GlobalUsings.cs
delete mode 100644 Content.IntegrationTests/ExternalTestContext.cs
delete mode 100644 Content.IntegrationTests/ITestContextLike.cs
delete mode 100644 Content.IntegrationTests/NUnitTestContextWrap.cs
delete mode 100644 Content.IntegrationTests/Pair/TestMapData.cs
delete mode 100644 Content.IntegrationTests/Pair/TestPair.Cvars.cs
delete mode 100644 Content.IntegrationTests/Pair/TestPair.Prototypes.cs
delete mode 100644 Content.IntegrationTests/Pair/TestPair.Timing.cs
delete mode 100644 Content.IntegrationTests/PoolManager.Prototypes.cs
delete mode 100644 Content.IntegrationTests/PoolTestLogHandler.cs
delete mode 100644 Content.IntegrationTests/TestPrototypesAttribute.cs
diff --git a/Content.Benchmarks/GlobalUsings.cs b/Content.Benchmarks/GlobalUsings.cs
new file mode 100644
index 0000000000..120b7f39b5
--- /dev/null
+++ b/Content.Benchmarks/GlobalUsings.cs
@@ -0,0 +1,3 @@
+// Global usings for Content.Benchmarks
+
+global using Robust.UnitTesting.Pool;
diff --git a/Content.IntegrationTests/ExternalTestContext.cs b/Content.IntegrationTests/ExternalTestContext.cs
deleted file mode 100644
index e23b2ee636..0000000000
--- a/Content.IntegrationTests/ExternalTestContext.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-///
-/// Generic implementation of for usage outside of actual tests.
-///
-public sealed class ExternalTestContext(string name, TextWriter writer) : ITestContextLike
-{
- public string FullName => name;
- public TextWriter Out => writer;
-}
diff --git a/Content.IntegrationTests/GlobalUsings.cs b/Content.IntegrationTests/GlobalUsings.cs
index 8422c5c3cd..1139d45dba 100644
--- a/Content.IntegrationTests/GlobalUsings.cs
+++ b/Content.IntegrationTests/GlobalUsings.cs
@@ -3,3 +3,4 @@
global using NUnit.Framework;
global using System;
global using System.Threading.Tasks;
+global using Robust.UnitTesting.Pool;
diff --git a/Content.IntegrationTests/ITestContextLike.cs b/Content.IntegrationTests/ITestContextLike.cs
deleted file mode 100644
index 47b6e08529..0000000000
--- a/Content.IntegrationTests/ITestContextLike.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-///
-/// Something that looks like a , for passing to integration tests.
-///
-public interface ITestContextLike
-{
- string FullName { get; }
- TextWriter Out { get; }
-}
-
diff --git a/Content.IntegrationTests/NUnitTestContextWrap.cs b/Content.IntegrationTests/NUnitTestContextWrap.cs
deleted file mode 100644
index 849c1b0910..0000000000
--- a/Content.IntegrationTests/NUnitTestContextWrap.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-///
-/// Canonical implementation of for usage in actual NUnit tests.
-///
-public sealed class NUnitTestContextWrap(TestContext context, TextWriter writer) : ITestContextLike
-{
- public string FullName => context.Test.FullName;
- public TextWriter Out => writer;
-}
diff --git a/Content.IntegrationTests/Pair/TestMapData.cs b/Content.IntegrationTests/Pair/TestMapData.cs
deleted file mode 100644
index 343641e161..0000000000
--- a/Content.IntegrationTests/Pair/TestMapData.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-
-namespace Content.IntegrationTests.Pair;
-
-///
-/// Simple data class that stored information about a map being used by a test.
-///
-public sealed class TestMapData
-{
- public EntityUid MapUid { get; set; }
- public Entity Grid;
- public MapId MapId;
- public EntityCoordinates GridCoords { get; set; }
- public MapCoordinates MapCoords { get; set; }
- public TileRef Tile { get; set; }
-
- // Client-side uids
- public EntityUid CMapUid { get; set; }
- public EntityUid CGridUid { get; set; }
- public EntityCoordinates CGridCoords { get; set; }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.Cvars.cs b/Content.IntegrationTests/Pair/TestPair.Cvars.cs
deleted file mode 100644
index 81df31fc9a..0000000000
--- a/Content.IntegrationTests/Pair/TestPair.Cvars.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using Content.Shared.CCVar;
-using Robust.Shared.Configuration;
-using Robust.Shared.Utility;
-
-namespace Content.IntegrationTests.Pair;
-
-public sealed partial class TestPair
-{
- private readonly Dictionary _modifiedClientCvars = new();
- private readonly Dictionary _modifiedServerCvars = new();
-
- private void OnServerCvarChanged(CVarChangeInfo args)
- {
- _modifiedServerCvars.TryAdd(args.Name, args.OldValue);
- }
-
- private void OnClientCvarChanged(CVarChangeInfo args)
- {
- _modifiedClientCvars.TryAdd(args.Name, args.OldValue);
- }
-
- internal void ClearModifiedCvars()
- {
- _modifiedClientCvars.Clear();
- _modifiedServerCvars.Clear();
- }
-
- ///
- /// Reverts any cvars that were modified during a test back to their original values.
- ///
- public async Task RevertModifiedCvars()
- {
- await Server.WaitPost(() =>
- {
- foreach (var (name, value) in _modifiedServerCvars)
- {
- if (Server.CfgMan.GetCVar(name).Equals(value))
- continue;
- Server.Log.Info($"Resetting cvar {name} to {value}");
- Server.CfgMan.SetCVar(name, value);
- }
-
- // I just love order dependent cvars
- if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik))
- Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik);
-
- });
-
- await Client.WaitPost(() =>
- {
- foreach (var (name, value) in _modifiedClientCvars)
- {
- if (Client.CfgMan.GetCVar(name).Equals(value))
- continue;
-
- var flags = Client.CfgMan.GetCVarFlags(name);
- if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER))
- continue;
-
- Client.Log.Info($"Resetting cvar {name} to {value}");
- Client.CfgMan.SetCVar(name, value);
- }
- });
-
- ClearModifiedCvars();
- }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
index 5e7ba0dcc8..1a3b38e829 100644
--- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -1,172 +1,19 @@
#nullable enable
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
-using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
// Contains misc helper functions to make writing tests easier.
public sealed partial class TestPair
{
- ///
- /// Creates a map, a grid, and a tile, and gives back references to them.
- ///
- [MemberNotNull(nameof(TestMap))]
- public async Task CreateTestMap(bool initialized = true, string tile = "Plating")
- {
- var mapData = new TestMapData();
- TestMap = mapData;
- await Server.WaitIdleAsync();
- var tileDefinitionManager = Server.ResolveDependency();
-
- TestMap = mapData;
- await Server.WaitPost(() =>
- {
- mapData.MapUid = Server.System().CreateMap(out mapData.MapId, runMapInit: initialized);
- mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
- mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
- var plating = tileDefinitionManager[tile];
- var platingTile = new Tile(plating.TileId);
- Server.System().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
- mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
- mapData.Tile = Server.System().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
- });
-
- TestMap = mapData;
- if (!Settings.Connected)
- return mapData;
-
- await RunTicksSync(10);
- mapData.CMapUid = ToClientUid(mapData.MapUid);
- mapData.CGridUid = ToClientUid(mapData.Grid);
- mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
-
- TestMap = mapData;
- return mapData;
- }
-
- ///
- /// Convert a client-side uid into a server-side uid
- ///
- public EntityUid ToServerUid(EntityUid uid) => ConvertUid(uid, Client, Server);
-
- ///
- /// Convert a server-side uid into a client-side uid
- ///
- public EntityUid ToClientUid(EntityUid uid) => ConvertUid(uid, Server, Client);
-
- private static EntityUid ConvertUid(
- EntityUid uid,
- RobustIntegrationTest.IntegrationInstance source,
- RobustIntegrationTest.IntegrationInstance destination)
- {
- if (!uid.IsValid())
- return EntityUid.Invalid;
-
- if (!source.EntMan.TryGetComponent(uid, out var meta))
- {
- Assert.Fail($"Failed to resolve MetaData while converting the EntityUid for entity {uid}");
- return EntityUid.Invalid;
- }
-
- if (!destination.EntMan.TryGetEntity(meta.NetEntity, out var otherUid))
- {
- Assert.Fail($"Failed to resolve net ID while converting the EntityUid entity {source.EntMan.ToPrettyString(uid)}");
- return EntityUid.Invalid;
- }
-
- return otherUid.Value;
- }
-
- ///
- /// Execute a command on the server and wait some number of ticks.
- ///
- public async Task WaitCommand(string cmd, int numTicks = 10)
- {
- await Server.ExecuteCommand(cmd);
- await RunTicksSync(numTicks);
- }
-
- ///
- /// Execute a command on the client and wait some number of ticks.
- ///
- public async Task WaitClientCommand(string cmd, int numTicks = 10)
- {
- await Client.ExecuteCommand(cmd);
- await RunTicksSync(numTicks);
- }
-
- ///
- /// Retrieve all entity prototypes that have some component.
- ///
- public List<(EntityPrototype, T)> GetPrototypesWithComponent(
- HashSet? ignored = null,
- bool ignoreAbstract = true,
- bool ignoreTestPrototypes = true)
- where T : IComponent, new()
- {
- if (!Server.ResolveDependency().TryGetRegistration(out var reg)
- && !Client.ResolveDependency().TryGetRegistration(out reg))
- {
- Assert.Fail($"Unknown component: {typeof(T).Name}");
- return new();
- }
-
- var id = reg.Name;
- var list = new List<(EntityPrototype, T)>();
- foreach (var proto in Server.ProtoMan.EnumeratePrototypes())
- {
- if (ignored != null && ignored.Contains(proto.ID))
- continue;
-
- if (ignoreAbstract && proto.Abstract)
- continue;
-
- if (ignoreTestPrototypes && IsTestPrototype(proto))
- continue;
-
- if (proto.Components.TryGetComponent(id, out var cmp))
- list.Add((proto, (T)cmp));
- }
-
- return list;
- }
-
- ///
- /// Retrieve all entity prototypes that have some component.
- ///
- public List GetPrototypesWithComponent(Type type,
- HashSet? ignored = null,
- bool ignoreAbstract = true,
- bool ignoreTestPrototypes = true)
- {
- var id = Server.ResolveDependency().GetComponentName(type);
- var list = new List();
- foreach (var proto in Server.ProtoMan.EnumeratePrototypes())
- {
- if (ignored != null && ignored.Contains(proto.ID))
- continue;
-
- if (ignoreAbstract && proto.Abstract)
- continue;
-
- if (ignoreTestPrototypes && IsTestPrototype(proto))
- continue;
-
- if (proto.Components.ContainsKey(id))
- list.Add((proto));
- }
-
- return list;
- }
+ public Task CreateTestMap(bool initialized = true)
+ => CreateTestMap(initialized, "Plating");
///
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
diff --git a/Content.IntegrationTests/Pair/TestPair.Prototypes.cs b/Content.IntegrationTests/Pair/TestPair.Prototypes.cs
deleted file mode 100644
index e50bc96d65..0000000000
--- a/Content.IntegrationTests/Pair/TestPair.Prototypes.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-using Robust.UnitTesting;
-
-namespace Content.IntegrationTests.Pair;
-
-// This partial class contains helper methods to deal with yaml prototypes.
-public sealed partial class TestPair
-{
- private Dictionary> _loadedPrototypes = new();
- private HashSet _loadedEntityPrototypes = new();
-
- public async Task LoadPrototypes(List prototypes)
- {
- await LoadPrototypes(Server, prototypes);
- await LoadPrototypes(Client, prototypes);
- }
-
- private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List prototypes)
- {
- var changed = new Dictionary>();
- foreach (var file in prototypes)
- {
- instance.ProtoMan.LoadString(file, changed: changed);
- }
-
- await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
-
- foreach (var (kind, ids) in changed)
- {
- _loadedPrototypes.GetOrNew(kind).UnionWith(ids);
- }
-
- if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
- _loadedEntityPrototypes.UnionWith(entIds);
- }
-
- public bool IsTestPrototype(EntityPrototype proto)
- {
- return _loadedEntityPrototypes.Contains(proto.ID);
- }
-
- public bool IsTestEntityPrototype(string id)
- {
- return _loadedEntityPrototypes.Contains(id);
- }
-
- public bool IsTestPrototype(string id) where TPrototype : IPrototype
- {
- return IsTestPrototype(typeof(TPrototype), id);
- }
-
- public bool IsTestPrototype(TPrototype proto) where TPrototype : IPrototype
- {
- return IsTestPrototype(typeof(TPrototype), proto.ID);
- }
-
- public bool IsTestPrototype(Type kind, string id)
- {
- return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
- }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
index 694d6cfa64..887361a872 100644
--- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
@@ -8,84 +8,17 @@ using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Preferences;
-using Robust.Client;
-using Robust.Server.Player;
-using Robust.Shared.Exceptions;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Network;
-using Robust.Shared.Utility;
+using Robust.Shared.Player;
namespace Content.IntegrationTests.Pair;
// This partial class contains logic related to recycling & disposing test pairs.
-public sealed partial class TestPair : IAsyncDisposable
+public sealed partial class TestPair
{
- public PairState State { get; private set; } = PairState.Ready;
-
- private async Task OnDirtyDispose()
+ protected override async Task Cleanup()
{
- 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()
- {
- await Server.WaitIdleAsync();
- await Client.WaitIdleAsync();
+ await base.Cleanup();
await ResetModifiedPreferences();
- await Server.RemoveAllDummySessions();
-
- if (TestMap != null)
- {
- await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
- TestMap = null;
- }
-
- await RevertModifiedCvars();
-
- var usageTime = Watch.Elapsed;
- Watch.Restart();
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
- // Let any last minute failures the test cause happen.
- await ReallyBeIdle();
- if (!Settings.Destructive)
- {
- if (Client.IsAlive == false)
- {
- throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);
- }
-
- if (Server.IsAlive == false)
- {
- throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
- }
- }
-
- if (Settings.MustNotBeReused)
- {
- Kill();
- await ReallyBeIdle();
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
- return;
- }
-
- var sRuntimeLog = Server.ResolveDependency();
- if (sRuntimeLog.ExceptionCount > 0)
- throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
- var cRuntimeLog = Client.ResolveDependency();
- if (cRuntimeLog.ExceptionCount > 0)
- throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
-
- var returnTime = Watch.Elapsed;
- await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
- State = PairState.Ready;
}
private async Task ResetModifiedPreferences()
@@ -95,61 +28,14 @@ public sealed partial class TestPair : IAsyncDisposable
{
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
}
+
_modifiedProfiles.Clear();
}
- public async ValueTask CleanReturnAsync()
+ protected override async Task Recycle(PairSettings next, TextWriter testOut)
{
- 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();
- DebugTools.Assert(State is PairState.Dead or PairState.Ready);
- PoolManager.NoCheckReturn(this);
- ClearContext();
- }
-
- public async ValueTask DisposeAsync()
- {
- switch (State)
- {
- case PairState.Dead:
- case PairState.Ready:
- break;
- case PairState.InUse:
- await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
- await OnDirtyDispose();
- PoolManager.NoCheckReturn(this);
- ClearContext();
- break;
- default:
- throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
- }
- }
-
- public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
- {
- Settings = default!;
- Watch.Restart();
- await testOut.WriteLineAsync($"Recycling...");
-
- var gameTicker = Server.System();
- var cNetMgr = Client.ResolveDependency();
-
- await RunTicksSync(1);
-
- // Disconnect the client if they are connected.
- if (cNetMgr.IsConnected)
- {
- await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
- await Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
- await RunTicksSync(1);
- }
- Assert.That(cNetMgr.IsConnected, Is.False);
-
// Move to pre-round lobby. Required to toggle dummy ticker on and off
+ var gameTicker = Server.System();
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
@@ -162,8 +48,7 @@ public sealed partial class TestPair : IAsyncDisposable
//Apply Cvars
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
- await PoolManager.SetupCVars(Client, settings);
- await PoolManager.SetupCVars(Server, settings);
+ await ApplySettings(next);
await RunTicksSync(1);
// Restart server.
@@ -171,52 +56,30 @@ public sealed partial class TestPair : IAsyncDisposable
await Server.WaitPost(() => Server.EntMan.FlushEntities());
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)
+ public override void ValidateSettings(PairSettings s)
{
+ base.ValidateSettings(s);
+ var settings = (PoolSettings) s;
+
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));
+ Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.DummyTicker));
- var entMan = Server.ResolveDependency();
- var ticker = entMan.System();
- Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
+ var ticker = Server.System();
+ Assert.That(ticker.DummyTicker, Is.EqualTo(settings.DummyTicker));
var expectPreRound = settings.InLobby | settings.DummyTicker;
var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
- var baseClient = Client.ResolveDependency();
- var netMan = Client.ResolveDependency();
- Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
-
- if (!settings.ShouldBeConnected)
+ if (ticker.DummyTicker || !settings.Connected)
return;
- Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
- var cPlayer = Client.ResolveDependency();
- var sPlayer = Server.ResolveDependency();
- Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
+ var sPlayer = Server.ResolveDependency();
var session = sPlayer.Sessions.Single();
- Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
-
- if (ticker.DummyTicker)
- return;
-
var status = ticker.PlayerGameStatuses[session.UserId];
var expected = settings.InLobby
? PlayerGameStatus.NotReadyToPlay
@@ -231,11 +94,11 @@ public sealed partial class TestPair : IAsyncDisposable
}
Assert.That(session.AttachedEntity, Is.Not.Null);
- Assert.That(entMan.EntityExists(session.AttachedEntity));
- Assert.That(entMan.HasComponent(session.AttachedEntity));
- var mindCont = entMan.GetComponent(session.AttachedEntity!.Value);
+ Assert.That(Server.EntMan.EntityExists(session.AttachedEntity));
+ Assert.That(Server.EntMan.HasComponent(session.AttachedEntity));
+ var mindCont = Server.EntMan.GetComponent(session.AttachedEntity!.Value);
Assert.That(mindCont.Mind, Is.Not.Null);
- Assert.That(entMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
+ Assert.That(Server.EntMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
Assert.That(mind!.VisitingEntity, Is.Null);
Assert.That(mind.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
Assert.That(mind.UserId, Is.EqualTo(session.UserId));
diff --git a/Content.IntegrationTests/Pair/TestPair.Timing.cs b/Content.IntegrationTests/Pair/TestPair.Timing.cs
deleted file mode 100644
index e0859660d4..0000000000
--- a/Content.IntegrationTests/Pair/TestPair.Timing.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-#nullable enable
-
-namespace Content.IntegrationTests.Pair;
-
-// This partial class contains methods for running the server/client pairs for some number of ticks
-public sealed partial class TestPair
-{
- ///
- /// Runs the server-client pair in sync
- ///
- /// How many ticks to run them for
- public async Task RunTicksSync(int ticks)
- {
- for (var i = 0; i < ticks; i++)
- {
- await Server.WaitRunTicks(1);
- await Client.WaitRunTicks(1);
- }
- }
-
- ///
- /// Convert a time interval to some number of ticks.
- ///
- public int SecondsToTicks(float seconds)
- {
- return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
- }
-
- ///
- /// Run the server & client in sync for some amount of time
- ///
- public async Task RunSeconds(float seconds)
- {
- await RunTicksSync(SecondsToTicks(seconds));
- }
-
- ///
- /// Runs the server-client pair in sync, but also ensures they are both idle each tick.
- ///
- /// How many ticks to run
- public async Task ReallyBeIdle(int runTicks = 25)
- {
- for (var i = 0; i < runTicks; i++)
- {
- await Client.WaitRunTicks(1);
- await Server.WaitRunTicks(1);
- for (var idleCycles = 0; idleCycles < 4; idleCycles++)
- {
- await Client.WaitIdleAsync();
- await Server.WaitIdleAsync();
- }
- }
- }
-
- ///
- /// Run the server/clients until the ticks are synchronized.
- /// By default the client will be one tick ahead of the server.
- ///
- public async Task SyncTicks(int targetDelta = 1)
- {
- var sTick = (int)Server.Timing.CurTick.Value;
- var cTick = (int)Client.Timing.CurTick.Value;
- var delta = cTick - sTick;
-
- if (delta == targetDelta)
- return;
- if (delta > targetDelta)
- await Server.WaitRunTicks(delta - targetDelta);
- else
- await Client.WaitRunTicks(targetDelta - delta);
-
- sTick = (int)Server.Timing.CurTick.Value;
- cTick = (int)Client.Timing.CurTick.Value;
- delta = cTick - sTick;
- Assert.That(delta, Is.EqualTo(targetDelta));
- }
-}
diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs
index 43b188fd32..947840d5ce 100644
--- a/Content.IntegrationTests/Pair/TestPair.cs
+++ b/Content.IntegrationTests/Pair/TestPair.cs
@@ -1,16 +1,17 @@
#nullable enable
using System.Collections.Generic;
-using System.IO;
-using System.Linq;
+using Content.Client.IoC;
+using Content.Client.Parallax.Managers;
+using Content.IntegrationTests.Tests.Destructible;
+using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.Server.GameTicking;
+using Content.Shared.CCVar;
using Content.Shared.Players;
-using Robust.Shared.Configuration;
+using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
+using Robust.Shared.Log;
using Robust.Shared.Network;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
@@ -18,156 +19,99 @@ namespace Content.IntegrationTests.Pair;
///
/// This object wraps a pooled server+client pair.
///
-public sealed partial class TestPair
+public sealed partial class TestPair : RobustIntegrationTest.TestPair
{
- public readonly int Id;
- private bool _initialized;
- private TextWriter _testOut = default!;
- public readonly Stopwatch Watch = new();
- public readonly List TestHistory = new();
- public PoolSettings Settings = default!;
- public TestMapData? TestMap;
private List _modifiedProfiles = new();
- private int _nextServerSeed;
- private int _nextClientSeed;
-
- public int ServerSeed;
- public int ClientSeed;
-
- public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
- public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
-
- public void Deconstruct(
- out RobustIntegrationTest.ServerIntegrationInstance server,
- out RobustIntegrationTest.ClientIntegrationInstance client)
- {
- server = Server;
- client = Client;
- }
-
- public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
-
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
- public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
- public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
-
- public TestPair(int id)
+ protected override async Task Initialize()
{
- Id = id;
- }
-
- public async Task Initialize(PoolSettings settings, TextWriter testOut, List testPrototypes)
- {
- if (_initialized)
- throw new InvalidOperationException("Already initialized");
-
- _initialized = true;
- Settings = settings;
- (Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
- (Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
- ActivateContext(testOut);
-
- Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
- Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
-
- if (!settings.NoLoadTestPrototypes)
- await LoadPrototypes(testPrototypes!);
-
- if (!settings.UseDummyTicker)
+ var settings = (PoolSettings)Settings;
+ if (!settings.DummyTicker)
{
- var gameTicker = Server.ResolveDependency().System();
+ var gameTicker = Server.System();
await Server.WaitPost(() => gameTicker.RestartRound());
}
+ }
- // Always initially connect clients to generate an initial random set of preferences/profiles.
- // This is to try and prevent issues where if the first test that connects the client is consistently some test
- // that uses a fixed seed, it would effectively prevent it from beingrandomized.
+ public override async Task RevertModifiedCvars()
+ {
+ // I just love order dependent cvars
+ // I.e., cvars that when changed automatically cause others to also change.
+ var modified = ModifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik);
- Client.SetConnectTarget(Server);
- await Client.WaitIdleAsync();
- var netMgr = Client.ResolveDependency();
- await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
- await ReallyBeIdle(10);
- await Client.WaitRunTicks(1);
+ await base.RevertModifiedCvars();
- if (!settings.ShouldBeConnected)
+ if (!modified)
+ return;
+
+ await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
+ ClearModifiedCvars();
+ }
+
+ protected override async Task ApplySettings(IIntegrationInstance instance, PairSettings n)
+ {
+ var next = (PoolSettings)n;
+ await base.ApplySettings(instance, next);
+ var cfg = instance.CfgMan;
+ await instance.WaitPost(() =>
{
- await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
- await ReallyBeIdle(10);
- }
+ if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
+ cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
- var cRand = Client.ResolveDependency();
- var sRand = Server.ResolveDependency();
- _nextClientSeed = cRand.Next();
- _nextServerSeed = sRand.Next();
+ if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
+ cfg.SetCVar(CCVars.GameLobbyEnabled, next.InLobby);
+
+ if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
+ cfg.SetCVar(CCVars.GameMap, next.Map);
+
+ if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
+ cfg.SetCVar(CCVars.AdminLogsEnabled, next.AdminLogsEnabled);
+ });
}
- public void Kill()
+ protected override RobustIntegrationTest.ClientIntegrationOptions ClientOptions()
{
- State = PairState.Dead;
- ServerLogHandler.ShuttingDown = true;
- ClientLogHandler.ShuttingDown = true;
- Server.Dispose();
- Client.Dispose();
- }
+ var opts = base.ClientOptions();
- 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,
- }
-
- public void SetupSeed()
- {
- var sRand = Server.ResolveDependency();
- if (Settings.ServerSeed is { } severSeed)
+ opts.LoadTestAssembly = false;
+ opts.ContentStart = true;
+ opts.FailureLogLevel = LogLevel.Warning;
+ opts.Options = new()
{
- ServerSeed = severSeed;
- sRand.SetSeed(ServerSeed);
- }
- else
- {
- ServerSeed = _nextServerSeed;
- sRand.SetSeed(ServerSeed);
- _nextServerSeed = sRand.Next();
- }
+ LoadConfigAndUserData = false,
+ };
- var cRand = Client.ResolveDependency();
- if (Settings.ClientSeed is { } clientSeed)
+ opts.BeforeStart += () =>
{
- ClientSeed = clientSeed;
- cRand.SetSeed(ClientSeed);
- }
- else
+ IoCManager.Resolve().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
+ {
+ ClientBeforeIoC = () => IoCManager.Register(true)
+ });
+ };
+ return opts;
+ }
+
+ protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
+ {
+ var opts = base.ServerOptions();
+
+ opts.LoadTestAssembly = false;
+ opts.ContentStart = true;
+ opts.Options = new()
{
- ClientSeed = _nextClientSeed;
- cRand.SetSeed(ClientSeed);
- _nextClientSeed = cRand.Next();
- }
+ LoadConfigAndUserData = false,
+ };
+
+ opts.BeforeStart += () =>
+ {
+ // Server-only systems (i.e., systems that subscribe to events with server-only components)
+ // There's probably a better way to do this.
+ var entSysMan = IoCManager.Resolve();
+ entSysMan.LoadExtraSystemType();
+ entSysMan.LoadExtraSystemType();
+ };
+ return opts;
}
}
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
index 8cf2b626dc..b457d4a40b 100644
--- a/Content.IntegrationTests/PoolManager.Cvars.cs
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -1,15 +1,14 @@
#nullable enable
using Content.Shared.CCVar;
-using Robust.Shared;
-using Robust.Shared.Configuration;
-using Robust.UnitTesting;
namespace Content.IntegrationTests;
-// Partial class containing cvar logic
+// Partial class containing test cvars
+// This could probably be merged into the main file, but I'm keeping it separate to reduce
+// conflicts for forks.
public static partial class PoolManager
{
- private static readonly (string cvar, string value)[] TestCvars =
+ public static readonly (string cvar, string value)[] TestCvars =
{
// @formatter:off
(CCVars.DatabaseSynchronous.Name, "true"),
@@ -17,9 +16,7 @@ public static partial class PoolManager
(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.GameRoleLoadoutTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
@@ -30,49 +27,13 @@ public static partial class PoolManager
(CCVars.ProcgenPreload.Name, "false"),
(CCVars.WorldgenEnabled.Name, "false"),
(CCVars.GatewayGeneratorEnabled.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"),
(CCVars.AutosaveEnabled.Name, "false"),
- (CVars.NetBufferSize.Name, "0"),
(CCVars.InteractionRateLimitCount.Name, "9999999"),
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
(CCVars.MovementMobPushing.Name, "false"),
};
-
- public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
- {
- var cfg = instance.ResolveDependency();
- await instance.WaitPost(() =>
- {
- if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
- cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
-
- if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
- cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
-
- if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
- cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
-
- if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
- cfg.SetCVar(CCVars.GameMap, settings.Map);
-
- if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
- cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
-
- if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
- cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
- });
- }
-
- private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
- {
- foreach (var (cvar, value) in TestCvars)
- {
- options.CVarOverrides[cvar] = value;
- }
- }
}
diff --git a/Content.IntegrationTests/PoolManager.Prototypes.cs b/Content.IntegrationTests/PoolManager.Prototypes.cs
deleted file mode 100644
index eb7518ea15..0000000000
--- a/Content.IntegrationTests/PoolManager.Prototypes.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using System.Reflection;
-using Robust.Shared.Utility;
-
-namespace Content.IntegrationTests;
-
-// Partial class for handling the discovering and storing test prototypes.
-public static partial class PoolManager
-{
- private static List _testPrototypes = new();
-
- private const BindingFlags Flags = BindingFlags.Static
- | BindingFlags.NonPublic
- | BindingFlags.Public
- | BindingFlags.DeclaredOnly;
-
- private static void DiscoverTestPrototypes(Assembly assembly)
- {
- foreach (var type in assembly.GetTypes())
- {
- foreach (var field in type.GetFields(Flags))
- {
- if (!field.HasCustomAttribute())
- continue;
-
- var val = field.GetValue(null);
- if (val is not string str)
- throw new Exception($"TestPrototypeAttribute is only valid on non-null string fields");
-
- _testPrototypes.Add(str);
- }
- }
- }
-}
diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs
index 64aac16751..6e0df92ad4 100644
--- a/Content.IntegrationTests/PoolManager.cs
+++ b/Content.IntegrationTests/PoolManager.cs
@@ -1,373 +1,17 @@
#nullable enable
-using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Reflection;
-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;
-using Content.IntegrationTests.Tests.Interaction.Click;
-using Robust.Client;
-using Robust.Server;
-using Robust.Shared.Configuration;
-using Robust.Shared.ContentPack;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Log;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
+using Content.Shared.CCVar;
using Robust.UnitTesting;
namespace Content.IntegrationTests;
-///
-/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
-///
+// The static class exist to avoid breaking changes
public static partial class PoolManager
{
+ public static readonly ContentPoolManager Instance = new();
public const string TestMap = "Empty";
- private static int _pairId;
- private static readonly object PairLock = new();
- private static bool _initialized;
-
- // Pair, IsBorrowed
- private static readonly Dictionary Pairs = new();
- private static bool _dead;
- private static Exception? _poolFailureReason;
-
- private static HashSet _contentAssemblies = default!;
-
- public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
- PoolSettings poolSettings,
- TextWriter testOut)
- {
- var options = new RobustIntegrationTest.ServerIntegrationOptions
- {
- ContentStart = true,
- Options = new ServerOptions()
- {
- LoadConfigAndUserData = false,
- LoadContentResources = !poolSettings.NoLoadContent,
- },
- ContentAssemblies = _contentAssemblies.ToArray()
- };
-
- var logHandler = new PoolTestLogHandler("SERVER");
- logHandler.ActivateContext(testOut);
- options.OverrideLogHandler = () => logHandler;
-
- options.BeforeStart += () =>
- {
- // Server-only systems (i.e., systems that subscribe to events with server-only components)
- var entSysMan = IoCManager.Resolve();
- entSysMan.LoadExtraSystemType();
- entSysMan.LoadExtraSystemType();
-
- IoCManager.Resolve().GetSawmill("loc").Level = LogLevel.Error;
- IoCManager.Resolve()
- .OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
- };
-
- SetDefaultCVars(options);
- var server = new RobustIntegrationTest.ServerIntegrationInstance(options);
- await server.WaitIdleAsync();
- await SetupCVars(server, poolSettings);
- return (server, logHandler);
- }
-
- ///
- /// This shuts down the pool, and disposes all the server/client pairs.
- /// This is a one time operation to be used when the testing program is exiting.
- ///
- public static void Shutdown()
- {
- List localPairs;
- lock (PairLock)
- {
- if (_dead)
- return;
- _dead = true;
- localPairs = Pairs.Keys.ToList();
- }
-
- foreach (var pair in localPairs)
- {
- pair.Kill();
- }
-
- _initialized = false;
- }
-
- public static string DeathReport()
- {
- lock (PairLock)
- {
- var builder = new StringBuilder();
- var pairs = Pairs.Keys.OrderBy(pair => pair.Id);
- foreach (var pair in pairs)
- {
- var borrowed = Pairs[pair];
- 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]}");
- }
- }
-
- return builder.ToString();
- }
- }
-
- public static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
- PoolSettings poolSettings,
- TextWriter testOut)
- {
- var options = new RobustIntegrationTest.ClientIntegrationOptions
- {
- FailureLogLevel = LogLevel.Warning,
- ContentStart = true,
- ContentAssemblies = new[]
- {
- typeof(Shared.Entry.EntryPoint).Assembly,
- typeof(Client.Entry.EntryPoint).Assembly,
- typeof(PoolManager).Assembly,
- }
- };
-
- if (poolSettings.NoLoadContent)
- {
- Assert.Warn("NoLoadContent does not work on the client, ignoring");
- }
-
- options.Options = new GameControllerOptions()
- {
- LoadConfigAndUserData = false,
- // LoadContentResources = !poolSettings.NoLoadContent
- };
-
- var logHandler = new PoolTestLogHandler("CLIENT");
- logHandler.ActivateContext(testOut);
- options.OverrideLogHandler = () => logHandler;
-
- options.BeforeStart += () =>
- {
- IoCManager.Resolve().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
- {
- ClientBeforeIoC = () =>
- {
- // do not register extra systems or components here -- they will get cleared when the client is
- // disconnected. just use reflection.
- IoCManager.Register(true);
- IoCManager.Resolve().GetSawmill("loc").Level = LogLevel.Error;
- IoCManager.Resolve()
- .OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
- }
- });
- };
-
- SetDefaultCVars(options);
- var client = new RobustIntegrationTest.ClientIntegrationInstance(options);
- await client.WaitIdleAsync();
- await SetupCVars(client, poolSettings);
- return (client, logHandler);
- }
-
- ///
- /// Gets a , which can be used to get access to a server, and client
- ///
- /// See
- ///
- public static async Task GetServerClient(
- PoolSettings? poolSettings = null,
- ITestContextLike? testContext = null)
- {
- return await GetServerClientPair(
- poolSettings ?? new PoolSettings(),
- testContext ?? new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out));
- }
-
- private static string GetDefaultTestName(ITestContextLike testContext)
- {
- return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
- }
-
- private static async Task GetServerClientPair(
- PoolSettings poolSettings,
- ITestContextLike testContext)
- {
- if (!_initialized)
- throw new InvalidOperationException($"Pool manager has not been initialized");
-
- // Trust issues with the AsyncLocal that backs this.
- var testOut = testContext.Out;
-
- DieIfPoolFailure();
- var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);
- var poolRetrieveTimeWatch = new Stopwatch();
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
- TestPair? pair = null;
- try
- {
- poolRetrieveTimeWatch.Start();
- if (poolSettings.MustBeNew)
- {
- await testOut.WriteLineAsync(
- $"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
- pair = await CreateServerClientPair(poolSettings, testOut);
- }
- else
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
- pair = GrabOptimalPair(poolSettings);
- if (pair != null)
- {
- pair.ActivateContext(testOut);
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
- var canSkip = pair.Settings.CanFastRecycle(poolSettings);
-
- if (canSkip)
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
- await SetupCVars(pair.Client, poolSettings);
- await SetupCVars(pair.Server, poolSettings);
- await pair.RunTicksSync(1);
- }
- else
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
- await pair.CleanPooledPair(poolSettings, testOut);
- }
-
- await pair.RunTicksSync(5);
- await pair.SyncTicks(targetDelta: 1);
- }
- else
- {
- await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
- pair = await CreateServerClientPair(poolSettings, testOut);
- }
- }
- }
- finally
- {
- if (pair != null && pair.TestHistory.Count > 0)
- {
- 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.Id} Test #{i}: {pair.TestHistory[i]}");
- }
- 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.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
-
- pair.ClearModifiedCvars();
- pair.Settings = poolSettings;
- pair.TestHistory.Add(currentTestName);
- pair.SetupSeed();
- await testOut.WriteLineAsync(
- $"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
-
- pair.Watch.Restart();
- return pair;
- }
-
- private static TestPair? GrabOptimalPair(PoolSettings poolSettings)
- {
- lock (PairLock)
- {
- 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;
- }
-
- return fallback;
- }
- }
-
- ///
- /// Used by TestPair after checking the server/client pair, Don't use this.
- ///
- public static void NoCheckReturn(TestPair pair)
- {
- lock (PairLock)
- {
- if (pair.State == TestPair.PairState.Dead)
- Pairs.Remove(pair);
- 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 void DieIfPoolFailure()
- {
- if (_poolFailureReason != null)
- {
- // If the _poolFailureReason is not null, we can assume at least one test failed.
- // So we say inconclusive so we don't add more failed tests to search through.
- Assert.Inconclusive(@$"
-In a different test, the pool manager had an exception when trying to create a server/client pair.
-Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
-we are just going to end this here to save a lot of time. This is the exception that started this:\n {_poolFailureReason}");
- }
-
- if (_dead)
- {
- // If Pairs is null, we ran out of time, we can't assume a test failed.
- // So we are going to tell it all future tests are a failure.
- Assert.Fail("The pool was shut down");
- }
- }
-
- private static async Task CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
- {
- try
- {
- 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;
- }
- }
///
/// Runs a server, or a client until a condition is true
@@ -423,29 +67,42 @@ we are just going to end this here to save a lot of time. This is the exception
Assert.That(passed);
}
- ///
- /// Initialize the pool manager.
- ///
- /// Assemblies to search for to discover extra prototypes and systems.
- public static void Startup(params Assembly[] extraAssemblies)
+ public static async Task GetServerClient(
+ PoolSettings? settings = null,
+ ITestContextLike? testContext = null)
{
- if (_initialized)
- throw new InvalidOperationException("Already initialized");
+ return await Instance.GetPair(settings, testContext);
+ }
- _initialized = true;
- _contentAssemblies =
- [
- typeof(Shared.Entry.EntryPoint).Assembly,
- typeof(Server.Entry.EntryPoint).Assembly,
- typeof(PoolManager).Assembly
- ];
- _contentAssemblies.UnionWith(extraAssemblies);
+ public static void Startup(params Assembly[] extra)
+ => Instance.Startup(extra);
- _testPrototypes.Clear();
- DiscoverTestPrototypes(typeof(PoolManager).Assembly);
- foreach (var assembly in extraAssemblies)
- {
- DiscoverTestPrototypes(assembly);
- }
+ public static void Shutdown() => Instance.Shutdown();
+ public static string DeathReport() => Instance.DeathReport();
+}
+
+///
+/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
+///
+public sealed class ContentPoolManager : PoolManager
+{
+ public override PairSettings DefaultSettings => new PoolSettings();
+ protected override string GetDefaultTestName(ITestContextLike testContext)
+ {
+ return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
+ }
+
+ public override void Startup(params Assembly[] extraAssemblies)
+ {
+ DefaultCvars.AddRange(PoolManager.TestCvars);
+
+ var shared = extraAssemblies
+ .Append(typeof(Shared.Entry.EntryPoint).Assembly)
+ .Append(typeof(PoolManager).Assembly)
+ .ToArray();
+
+ Startup([typeof(Client.Entry.EntryPoint).Assembly],
+ [typeof(Server.Entry.EntryPoint).Assembly],
+ shared);
}
}
diff --git a/Content.IntegrationTests/PoolSettings.cs b/Content.IntegrationTests/PoolSettings.cs
index 9da514e66b..fe37c38fe3 100644
--- a/Content.IntegrationTests/PoolSettings.cs
+++ b/Content.IntegrationTests/PoolSettings.cs
@@ -1,43 +1,31 @@
-#nullable enable
+namespace Content.IntegrationTests;
-using Robust.Shared.Random;
-
-namespace Content.IntegrationTests;
-
-///
-/// Settings for the pooled server, and client pair.
-/// Some options are for changing the pair, and others are
-/// so the pool can properly clean up what you borrowed.
-///
-public sealed class PoolSettings
+///
+public sealed class PoolSettings : PairSettings
{
- ///
- /// Set to true if the test will ruin the server/client pair.
- ///
- public bool Destructive { get; init; }
+ public override bool Connected
+ {
+ get => _connected || InLobby;
+ init => _connected = value;
+ }
- ///
- /// Set to true if the given server/client pair should be created fresh.
- ///
- public bool Fresh { get; init; }
+ private readonly bool _dummyTicker = true;
+ private readonly bool _connected;
///
/// Set to true if the given server should be using a dummy ticker. Ignored if is true.
///
- public bool DummyTicker { get; init; } = true;
+ public bool DummyTicker
+ {
+ get => _dummyTicker && !InLobby;
+ init => _dummyTicker = value;
+ }
///
/// If true, this enables the creation of admin logs during the test.
///
public bool AdminLogsEnabled { get; init; }
- ///
- /// Set to true if the given server/client pair should be connected from each other.
- /// Defaults to disconnected as it makes dirty recycling slightly faster.
- /// If is true, this option is ignored.
- ///
- public bool Connected { get; init; }
-
///
/// 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.
@@ -53,81 +41,22 @@ public sealed class PoolSettings
///
public bool NoLoadContent { get; init; }
- ///
- /// This will return a server-client pair that has not loaded test prototypes.
- /// Try avoiding this whenever possible, as this will always create & destroy a new pair.
- /// Use if you need to exclude test prototypees.
- ///
- public bool NoLoadTestPrototypes { get; init; }
-
- ///
- /// Set this to true to disable the NetInterp CVar on the given server/client pair
- ///
- public bool DisableInterpolate { get; init; }
-
- ///
- /// Set this to true to always clean up the server/client pair before giving it to another borrower
- ///
- public bool Dirty { get; init; }
-
///
/// Set this to the path of a map to have the given server/client pair load the map.
///
public string Map { get; init; } = PoolManager.TestMap;
- ///
- /// Overrides the test name detection, and uses this in the test history instead
- ///
- public string? TestName { get; set; }
-
- ///
- /// If set, this will be used to call
- ///
- public int? ServerSeed { get; set; }
-
- ///
- /// If set, this will be used to call
- ///
- public int? ClientSeed { get; set; }
-
- #region Inferred Properties
-
- ///
- /// If the returned pair must not be reused
- ///
- public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
-
- ///
- /// If the given pair must be brand new
- ///
- public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
-
- public bool UseDummyTicker => !InLobby && DummyTicker;
-
- public bool ShouldBeConnected => InLobby || Connected;
-
- #endregion
-
- ///
- /// Tries to guess if we can skip recycling the server/client pair.
- ///
- /// The next set of settings the old pair will be set to
- /// If we can skip cleaning it up
- public bool CanFastRecycle(PoolSettings nextSettings)
+ public override bool CanFastRecycle(PairSettings nextSettings)
{
- if (MustNotBeReused)
- throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
+ if (!base.CanFastRecycle(nextSettings))
+ return false;
- if (nextSettings.MustBeNew)
- throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
-
- if (Dirty)
+ if (nextSettings is not PoolSettings next)
return false;
// Check that certain settings match.
- return !ShouldBeConnected == !nextSettings.ShouldBeConnected
- && UseDummyTicker == nextSettings.UseDummyTicker
- && Map == nextSettings.Map
- && InLobby == nextSettings.InLobby;
+ return DummyTicker == next.DummyTicker
+ && Map == next.Map
+ && InLobby == next.InLobby;
}
}
diff --git a/Content.IntegrationTests/PoolTestLogHandler.cs b/Content.IntegrationTests/PoolTestLogHandler.cs
deleted file mode 100644
index 909bee9785..0000000000
--- a/Content.IntegrationTests/PoolTestLogHandler.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System.IO;
-using Robust.Shared.Log;
-using Robust.Shared.Timing;
-using Serilog.Events;
-
-namespace Content.IntegrationTests;
-
-#nullable enable
-
-///
-/// Log handler intended for pooled integration tests.
-///
-///
-///
-/// This class logs to two places: an NUnit
-/// (so it nicely gets attributed to a test in your IDE),
-/// and an in-memory ring buffer for diagnostic purposes.
-/// If test pooling breaks, the ring buffer can be used to see what the broken instance has gone through.
-///
-///
-/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
-///
-///
-public sealed class PoolTestLogHandler : ILogHandler
-{
- private readonly string? _prefix;
-
- private RStopwatch _stopwatch;
-
- public TextWriter? ActiveContext { get; private set; }
-
- public LogLevel? FailureLevel { get; set; }
-
- public PoolTestLogHandler(string? prefix)
- {
- _prefix = prefix != null ? $"{prefix}: " : "";
- }
-
- public bool ShuttingDown;
-
- public void Log(string sawmillName, LogEvent message)
- {
- var level = message.Level.ToRobust();
-
- if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
- return;
-
- if (ActiveContext is not { } testContext)
- {
- // If this gets hit it means something is logging to this instance while it's "between" tests.
- // This is a bug in either the game or the testing system, and must always be investigated.
- throw new InvalidOperationException("Log to pool test log handler without active test context");
- }
-
- var name = LogMessage.LogLevelToName(level);
- var seconds = _stopwatch.Elapsed.TotalSeconds;
- var rendered = message.RenderMessage();
- var line = $"{_prefix}{seconds:F3}s [{name}] {sawmillName}: {rendered}";
-
- testContext.WriteLine(line);
-
- if (FailureLevel == null || level < FailureLevel)
- return;
-
- testContext.Flush();
- Assert.Fail($"{line} Exception: {message.Exception}");
- }
-
- public void ClearContext()
- {
- ActiveContext = null;
- }
-
- public void ActivateContext(TextWriter context)
- {
- _stopwatch.Restart();
- ActiveContext = context;
- }
-}
diff --git a/Content.IntegrationTests/TestPrototypesAttribute.cs b/Content.IntegrationTests/TestPrototypesAttribute.cs
deleted file mode 100644
index a6728d6728..0000000000
--- a/Content.IntegrationTests/TestPrototypesAttribute.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using JetBrains.Annotations;
-
-namespace Content.IntegrationTests;
-
-///
-/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
-///
-[AttributeUsage(AttributeTargets.Field)]
-[MeansImplicitUse]
-public sealed class TestPrototypesAttribute : Attribute
-{
-}
diff --git a/Content.MapRenderer/Painters/MapPainter.cs b/Content.MapRenderer/Painters/MapPainter.cs
index 991fa74fe1..a0198b35a0 100644
--- a/Content.MapRenderer/Painters/MapPainter.cs
+++ b/Content.MapRenderer/Painters/MapPainter.cs
@@ -20,6 +20,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
+using Robust.UnitTesting.Pool;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs
index 9d7843bcd0..534b12565c 100644
--- a/Content.MapRenderer/Program.cs
+++ b/Content.MapRenderer/Program.cs
@@ -9,6 +9,7 @@ using Content.IntegrationTests;
using Content.MapRenderer.Painters;
using Content.Server.Maps;
using Robust.Shared.Prototypes;
+using Robust.UnitTesting.Pool;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;