]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Move TestPair & PoolManager to engine (#36797)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Sun, 21 Sep 2025 05:17:43 +0000 (17:17 +1200)
committerGitHub <noreply@github.com>
Sun, 21 Sep 2025 05:17:43 +0000 (17:17 +1200)
* Move TestPair & PoolManager to engine

* Add to global usings

* A

* Move ITestContextLike to engine

* Readd cvars partial class

* cleanup diff

20 files changed:
Content.Benchmarks/GlobalUsings.cs [new file with mode: 0644]
Content.IntegrationTests/ExternalTestContext.cs [deleted file]
Content.IntegrationTests/GlobalUsings.cs
Content.IntegrationTests/ITestContextLike.cs [deleted file]
Content.IntegrationTests/NUnitTestContextWrap.cs [deleted file]
Content.IntegrationTests/Pair/TestMapData.cs [deleted file]
Content.IntegrationTests/Pair/TestPair.Cvars.cs [deleted file]
Content.IntegrationTests/Pair/TestPair.Helpers.cs
Content.IntegrationTests/Pair/TestPair.Prototypes.cs [deleted file]
Content.IntegrationTests/Pair/TestPair.Recycle.cs
Content.IntegrationTests/Pair/TestPair.Timing.cs [deleted file]
Content.IntegrationTests/Pair/TestPair.cs
Content.IntegrationTests/PoolManager.Cvars.cs
Content.IntegrationTests/PoolManager.Prototypes.cs [deleted file]
Content.IntegrationTests/PoolManager.cs
Content.IntegrationTests/PoolSettings.cs
Content.IntegrationTests/PoolTestLogHandler.cs [deleted file]
Content.IntegrationTests/TestPrototypesAttribute.cs [deleted file]
Content.MapRenderer/Painters/MapPainter.cs
Content.MapRenderer/Program.cs

diff --git a/Content.Benchmarks/GlobalUsings.cs b/Content.Benchmarks/GlobalUsings.cs
new file mode 100644 (file)
index 0000000..120b7f3
--- /dev/null
@@ -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 (file)
index e23b2ee..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-/// <summary>
-/// Generic implementation of <see cref="ITestContextLike"/> for usage outside of actual tests.
-/// </summary>
-public sealed class ExternalTestContext(string name, TextWriter writer) : ITestContextLike
-{
-    public string FullName => name;
-    public TextWriter Out => writer;
-}
index 8422c5c3cdc6864b6250186a6c73df9e4af1fd34..1139d45dba0fb8cf56e162d10c52be33db8968cd 100644 (file)
@@ -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 (file)
index 47b6e08..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-/// <summary>
-/// Something that looks like a <see cref="TestContext"/>, for passing to integration tests.
-/// </summary>
-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 (file)
index 849c1b0..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.IO;
-
-namespace Content.IntegrationTests;
-
-/// <summary>
-/// Canonical implementation of <see cref="ITestContextLike"/> for usage in actual NUnit tests.
-/// </summary>
-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 (file)
index 343641e..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-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 Entity<MapGridComponent> 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 (file)
index 81df31f..0000000
+++ /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<string, object> _modifiedClientCvars = new();
-    private readonly Dictionary<string, object> _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();
-    }
-
-    /// <summary>
-    /// Reverts any cvars that were modified during a test back to their original values.
-    /// </summary>
-    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();
-    }
-}
index 5e7ba0dcc83a10a3e48b16e013a622d9edd482c1..1a3b38e829710b2973c5d3f64d43ff10c72d31bc 100644 (file)
 #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
 {
-    /// <summary>
-    /// Creates a map, a grid, and a tile, and gives back references to them.
-    /// </summary>
-    [MemberNotNull(nameof(TestMap))]
-    public async Task<TestMapData> CreateTestMap(bool initialized = true, string tile = "Plating")
-    {
-        var mapData = new TestMapData();
-        TestMap = mapData;
-        await Server.WaitIdleAsync();
-        var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
-
-        TestMap = mapData;
-        await Server.WaitPost(() =>
-        {
-            mapData.MapUid = Server.System<SharedMapSystem>().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<SharedMapSystem>().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
-            mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
-            mapData.Tile = Server.System<SharedMapSystem>().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;
-    }
-
-    /// <summary>
-    /// Convert a client-side uid into a server-side uid
-    /// </summary>
-    public EntityUid ToServerUid(EntityUid uid) => ConvertUid(uid, Client, Server);
-
-    /// <summary>
-    /// Convert a server-side uid into a client-side uid
-    /// </summary>
-    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<MetaDataComponent>(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;
-    }
-
-    /// <summary>
-    /// Execute a command on the server and wait some number of ticks.
-    /// </summary>
-    public async Task WaitCommand(string cmd, int numTicks = 10)
-    {
-        await Server.ExecuteCommand(cmd);
-        await RunTicksSync(numTicks);
-    }
-
-    /// <summary>
-    /// Execute a command on the client and wait some number of ticks.
-    /// </summary>
-    public async Task WaitClientCommand(string cmd, int numTicks = 10)
-    {
-        await Client.ExecuteCommand(cmd);
-        await RunTicksSync(numTicks);
-    }
-
-    /// <summary>
-    /// Retrieve all entity prototypes that have some component.
-    /// </summary>
-    public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
-        HashSet<string>? ignored = null,
-        bool ignoreAbstract = true,
-        bool ignoreTestPrototypes = true)
-        where T : IComponent, new()
-    {
-        if (!Server.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out var reg)
-            && !Client.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(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<EntityPrototype>())
-        {
-            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;
-    }
-
-    /// <summary>
-    /// Retrieve all entity prototypes that have some component.
-    /// </summary>
-    public List<EntityPrototype> GetPrototypesWithComponent(Type type,
-        HashSet<string>? ignored = null,
-        bool ignoreAbstract = true,
-        bool ignoreTestPrototypes = true)
-    {
-        var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
-        var list = new List<EntityPrototype>();
-        foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
-        {
-            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<TestMapData> CreateTestMap(bool initialized = true)
+        => CreateTestMap(initialized, "Plating");
 
     /// <summary>
     /// 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 (file)
index e50bc96..0000000
+++ /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<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);
-    }
-}
index 694d6cfa64c98e5314455a9e23716905e79e3340..887361a87245411fd2b1eb83d38d188d3cade3a6 100644 (file)
@@ -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()
-    {
-        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()
+    protected override async Task Cleanup()
     {
-        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<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");
-        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()
-    {
-        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}.");
-        }
+        _modifiedProfiles.Clear();
     }
 
-    public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
+    protected override async Task Recycle(PairSettings next, 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
+        var gameTicker = Server.System<GameTicker>();
         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<EntityManager>();
-        var ticker = entMan.System<GameTicker>();
-        Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
+        var ticker = Server.System<GameTicker>();
+        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<IBaseClient>();
-        var netMan = Client.ResolveDependency<INetManager>();
-        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<Robust.Client.Player.IPlayerManager>();
-        var sPlayer = Server.ResolveDependency<IPlayerManager>();
-        Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
+        var sPlayer = Server.ResolveDependency<ISharedPlayerManager>();
         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<MindContainerComponent>(session.AttachedEntity));
-        var mindCont = entMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
+        Assert.That(Server.EntMan.EntityExists(session.AttachedEntity));
+        Assert.That(Server.EntMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
+        var mindCont = Server.EntMan.GetComponent<MindContainerComponent>(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 (file)
index e085966..0000000
+++ /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
-{
-    /// <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>
-    /// Convert a time interval to some number of ticks.
-    /// </summary>
-    public int SecondsToTicks(float seconds)
-    {
-        return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
-    }
-
-    /// <summary>
-    /// Run the server & client in sync for some amount of time
-    /// </summary>
-    public async Task RunSeconds(float seconds)
-    {
-        await RunTicksSync(SecondsToTicks(seconds));
-    }
-
-    /// <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));
-    }
-}
index 43b188fd32768372b00e8dfb75dfb556fe8fc385..947840d5cedad768c07272f23d94f7b1cbb0563c 100644 (file)
@@ -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;
 /// <summary>
 /// This object wraps a pooled server+client pair.
 /// </summary>
-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<string> TestHistory = new();
-    public PoolSettings Settings = default!;
-    public TestMapData? TestMap;
     private List<NetUserId> _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;
+        var settings = (PoolSettings)Settings;
+        if (!settings.DummyTicker)
+        {
+            var gameTicker = Server.System<GameTicker>();
+            await Server.WaitPost(() => gameTicker.RestartRound());
+        }
     }
 
-    public async Task Initialize(PoolSettings settings, TextWriter testOut, List<string> testPrototypes)
+    public override async Task RevertModifiedCvars()
     {
-        if (_initialized)
-            throw new InvalidOperationException("Already initialized");
+        // 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);
 
-        _initialized = true;
-        Settings = settings;
-        (Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
-        (Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
-        ActivateContext(testOut);
+        await base.RevertModifiedCvars();
 
-        Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
-        Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
+        if (!modified)
+            return;
 
-        if (!settings.NoLoadTestPrototypes)
-            await LoadPrototypes(testPrototypes!);
+        await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
+        ClearModifiedCvars();
+    }
 
-        if (!settings.UseDummyTicker)
+    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(() =>
         {
-            var gameTicker = Server.ResolveDependency<IEntityManager>().System<GameTicker>();
-            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.
+            if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
+                cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
 
-        Client.SetConnectTarget(Server);
-        await Client.WaitIdleAsync();
-        var netMgr = Client.ResolveDependency<IClientNetManager>();
-        await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
-        await ReallyBeIdle(10);
-        await Client.WaitRunTicks(1);
+            if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
+                cfg.SetCVar(CCVars.GameLobbyEnabled, next.InLobby);
 
-        if (!settings.ShouldBeConnected)
-        {
-            await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
-            await ReallyBeIdle(10);
-        }
+            if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
+                cfg.SetCVar(CCVars.GameMap, next.Map);
 
-        var cRand = Client.ResolveDependency<IRobustRandom>();
-        var sRand = Server.ResolveDependency<IRobustRandom>();
-        _nextClientSeed = cRand.Next();
-        _nextServerSeed = sRand.Next();
+            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();
-    }
+        opts.LoadTestAssembly = false;
+        opts.ContentStart = true;
+        opts.FailureLogLevel = LogLevel.Warning;
+        opts.Options = new()
+        {
+            LoadConfigAndUserData = false,
+        };
 
-    public void ActivateContext(TextWriter testOut)
-    {
-        _testOut = testOut;
-        ServerLogHandler.ActivateContext(testOut);
-        ClientLogHandler.ActivateContext(testOut);
+        opts.BeforeStart += () =>
+        {
+            IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
+                {
+                    ClientBeforeIoC = () => IoCManager.Register<IParallaxManager, DummyParallaxManager>(true)
+                });
+        };
+        return opts;
     }
 
-    public void Use()
+    protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
     {
-        if (State != PairState.Ready)
-            throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
-        State = PairState.InUse;
-    }
+        var opts = base.ServerOptions();
 
-    public enum PairState : byte
-    {
-        Ready = 0,
-        InUse = 1,
-        CleanDisposed = 2,
-        Dead = 3,
-    }
-
-    public void SetupSeed()
-    {
-        var sRand = Server.ResolveDependency<IRobustRandom>();
-        if (Settings.ServerSeed is { } severSeed)
-        {
-            ServerSeed = severSeed;
-            sRand.SetSeed(ServerSeed);
-        }
-        else
+        opts.LoadTestAssembly = false;
+        opts.ContentStart = true;
+        opts.Options = new()
         {
-            ServerSeed = _nextServerSeed;
-            sRand.SetSeed(ServerSeed);
-            _nextServerSeed = sRand.Next();
-        }
+            LoadConfigAndUserData = false,
+        };
 
-        var cRand = Client.ResolveDependency<IRobustRandom>();
-        if (Settings.ClientSeed is { } clientSeed)
-        {
-            ClientSeed = clientSeed;
-            cRand.SetSeed(ClientSeed);
-        }
-        else
+        opts.BeforeStart += () =>
         {
-            ClientSeed = _nextClientSeed;
-            cRand.SetSeed(ClientSeed);
-            _nextClientSeed = cRand.Next();
-        }
+            // 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<IEntitySystemManager>();
+            entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
+            entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
+        };
+        return opts;
     }
 }
index 8cf2b626dc6fb33824d2da7820a49eaf89dc68d6..b457d4a40bf1e74a8ce05048ca095eda71b2f296 100644 (file)
@@ -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<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;
-        }
-    }
 }
diff --git a/Content.IntegrationTests/PoolManager.Prototypes.cs b/Content.IntegrationTests/PoolManager.Prototypes.cs
deleted file mode 100644 (file)
index eb7518e..0000000
+++ /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<string> _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<TestPrototypesAttribute>())
-                    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);
-            }
-        }
-    }
-}
index 64aac16751cd9ec919d591a606640197e21630b5..6e0df92ad429a5a7e62dcad629ae244a1e6827e2 100644 (file)
 #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;
 
-/// <summary>
-/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
-/// </summary>
+// 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<TestPair, bool> Pairs = new();
-    private static bool _dead;
-    private static Exception? _poolFailureReason;
-
-    private static HashSet<Assembly> _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<IEntitySystemManager>();
-            entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
-            entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
-
-            IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
-            IoCManager.Resolve<IConfigurationManager>()
-                .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);
-    }
-
-    /// <summary>
-    /// 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.
-    /// </summary>
-    public static void Shutdown()
-    {
-        List<TestPair> 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<IModLoader>().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<IParallaxManager, DummyParallaxManager>(true);
-                    IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
-                    IoCManager.Resolve<IConfigurationManager>()
-                        .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);
-    }
-
-    /// <summary>
-    /// 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<TestPair> 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<TestPair> 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;
-        }
-    }
-
-    /// <summary>
-    /// Used by TestPair after checking the server/client pair, Don't use this.
-    /// </summary>
-    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<TestPair> 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;
-        }
-    }
 
     /// <summary>
     /// 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);
     }
 
-    /// <summary>
-    /// Initialize the pool manager.
-    /// </summary>
-    /// <param name="extraAssemblies">Assemblies to search for to discover extra prototypes and systems.</param>
-    public static void Startup(params Assembly[] extraAssemblies)
+    public static async Task<TestPair> 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();
+}
+
+/// <summary>
+/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
+/// </summary>
+public sealed class ContentPoolManager : PoolManager<TestPair>
+{
+    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);
     }
 }
index 9da514e66b89b6e66a93e9949a9a38fa5103ec2f..fe37c38fe369d998151788f15b97f7a20abf4b21 100644 (file)
@@ -1,43 +1,31 @@
-#nullable enable
+namespace Content.IntegrationTests;
 
-using Robust.Shared.Random;
-
-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
+/// <inheritdoc/>
+public sealed class PoolSettings : PairSettings
 {
-    /// <summary>
-    /// Set to true if the test will ruin the server/client pair.
-    /// </summary>
-    public bool Destructive { get; init; }
+    public override bool Connected
+    {
+        get => _connected || InLobby;
+        init => _connected = value;
+    }
 
-    /// <summary>
-    /// Set to true if the given server/client pair should be created fresh.
-    /// </summary>
-    public bool Fresh { get; init; }
+    private readonly bool _dummyTicker = true;
+    private readonly bool _connected;
 
     /// <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 DummyTicker
+    {
+        get => _dummyTicker && !InLobby;
+        init => _dummyTicker = value;
+    }
 
     /// <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; }
-
     /// <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.
@@ -53,81 +41,22 @@ public sealed class PoolSettings
     /// </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>
-    /// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
-    /// </summary>
-    public int? ServerSeed { get; set; }
-
-    /// <summary>
-    /// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
-    /// </summary>
-    public int? ClientSeed { get; set; }
-
-    #region Inferred Properties
-
-    /// <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;
-
-    public bool UseDummyTicker => !InLobby && DummyTicker;
-
-    public bool ShouldBeConnected => InLobby || Connected;
-
-    #endregion
-
-    /// <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)
+    public override bool CanFastRecycle(PairSettings 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 (!base.CanFastRecycle(nextSettings))
+            return false;
 
-        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 (file)
index 909bee9..0000000
+++ /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
-
-/// <summary>
-/// Log handler intended for pooled integration tests.
-/// </summary>
-/// <remarks>
-/// <para>
-/// This class logs to two places: an NUnit <see cref="TestContext"/>
-/// (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.
-/// </para>
-/// <para>
-/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
-/// </para>
-/// </remarks>
-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 (file)
index a6728d6..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using JetBrains.Annotations;
-
-namespace Content.IntegrationTests;
-
-/// <summary>
-/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
-/// </summary>
-[AttributeUsage(AttributeTargets.Field)]
-[MeansImplicitUse]
-public sealed class TestPrototypesAttribute : Attribute
-{
-}
index 991fa74fe1c85b5d9b1b81f7989154dc97aafeb8..a0198b35a06a9454699614041aa5081107d052f3 100644 (file)
@@ -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;
index 9d7843bcd07b4a6d6eef07bb547bfb30132d2556..534b12565c10d44b608f794c08d4cf4337b14c2d 100644 (file)
@@ -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;