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;