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
This commit is contained in:
3
Content.Benchmarks/GlobalUsings.cs
Normal file
3
Content.Benchmarks/GlobalUsings.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Global usings for Content.Benchmarks
|
||||
|
||||
global using Robust.UnitTesting.Pool;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,3 +3,4 @@
|
||||
global using NUnit.Framework;
|
||||
global using System;
|
||||
global using System.Threading.Tasks;
|
||||
global using Robust.UnitTesting.Pool;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<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()
|
||||
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<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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
public async Task Initialize(PoolSettings settings, TextWriter testOut, List<string> testPrototypes)
|
||||
var settings = (PoolSettings)Settings;
|
||||
if (!settings.DummyTicker)
|
||||
{
|
||||
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 gameTicker = Server.ResolveDependency<IEntityManager>().System<GameTicker>();
|
||||
var gameTicker = Server.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.
|
||||
|
||||
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 (!settings.ShouldBeConnected)
|
||||
{
|
||||
await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
|
||||
await ReallyBeIdle(10);
|
||||
}
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
var sRand = Server.ResolveDependency<IRobustRandom>();
|
||||
_nextClientSeed = cRand.Next();
|
||||
_nextServerSeed = sRand.Next();
|
||||
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);
|
||||
|
||||
await base.RevertModifiedCvars();
|
||||
|
||||
if (!modified)
|
||||
return;
|
||||
|
||||
await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
|
||||
ClearModifiedCvars();
|
||||
}
|
||||
|
||||
public void Kill()
|
||||
protected override async Task ApplySettings(IIntegrationInstance instance, PairSettings n)
|
||||
{
|
||||
State = PairState.Dead;
|
||||
ServerLogHandler.ShuttingDown = true;
|
||||
ClientLogHandler.ShuttingDown = true;
|
||||
Server.Dispose();
|
||||
Client.Dispose();
|
||||
var next = (PoolSettings)n;
|
||||
await base.ApplySettings(instance, next);
|
||||
var cfg = instance.CfgMan;
|
||||
await instance.WaitPost(() =>
|
||||
{
|
||||
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
|
||||
cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
private void ClearContext()
|
||||
protected override RobustIntegrationTest.ClientIntegrationOptions ClientOptions()
|
||||
{
|
||||
_testOut = default!;
|
||||
ServerLogHandler.ClearContext();
|
||||
ClientLogHandler.ClearContext();
|
||||
var opts = base.ClientOptions();
|
||||
|
||||
opts.LoadTestAssembly = false;
|
||||
opts.ContentStart = true;
|
||||
opts.FailureLogLevel = LogLevel.Warning;
|
||||
opts.Options = new()
|
||||
{
|
||||
LoadConfigAndUserData = false,
|
||||
};
|
||||
|
||||
opts.BeforeStart += () =>
|
||||
{
|
||||
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
|
||||
{
|
||||
ClientBeforeIoC = () => IoCManager.Register<IParallaxManager, DummyParallaxManager>(true)
|
||||
});
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
|
||||
public void ActivateContext(TextWriter testOut)
|
||||
protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
|
||||
{
|
||||
_testOut = testOut;
|
||||
ServerLogHandler.ActivateContext(testOut);
|
||||
ClientLogHandler.ActivateContext(testOut);
|
||||
}
|
||||
var opts = base.ServerOptions();
|
||||
|
||||
public void Use()
|
||||
opts.LoadTestAssembly = false;
|
||||
opts.ContentStart = true;
|
||||
opts.Options = new()
|
||||
{
|
||||
if (State != PairState.Ready)
|
||||
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
|
||||
State = PairState.InUse;
|
||||
}
|
||||
LoadConfigAndUserData = false,
|
||||
};
|
||||
|
||||
public enum PairState : byte
|
||||
opts.BeforeStart += () =>
|
||||
{
|
||||
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
|
||||
{
|
||||
ServerSeed = _nextServerSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
_nextServerSeed = sRand.Next();
|
||||
}
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
if (Settings.ClientSeed is { } clientSeed)
|
||||
{
|
||||
ClientSeed = clientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <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");
|
||||
|
||||
_initialized = true;
|
||||
_contentAssemblies =
|
||||
[
|
||||
typeof(Shared.Entry.EntryPoint).Assembly,
|
||||
typeof(Server.Entry.EntryPoint).Assembly,
|
||||
typeof(PoolManager).Assembly
|
||||
];
|
||||
_contentAssemblies.UnionWith(extraAssemblies);
|
||||
|
||||
_testPrototypes.Clear();
|
||||
DiscoverTestPrototypes(typeof(PoolManager).Assembly);
|
||||
foreach (var assembly in extraAssemblies)
|
||||
{
|
||||
DiscoverTestPrototypes(assembly);
|
||||
return await Instance.GetPair(settings, testContext);
|
||||
}
|
||||
|
||||
public static void Startup(params Assembly[] extra)
|
||||
=> Instance.Startup(extra);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (!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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user