mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 06:42:25 +02:00
Move TestPair & PoolManager to engine (#5877)
* Engine pool manager * Move documentation * Move namespace * Move TestMapData to engine * Option to prevent loading test assembly * release notes * Rename to avoid conflicts
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
public interface ITestPair
|
||||
{
|
||||
int Id { get; }
|
||||
public Stopwatch Watch { get; }
|
||||
public PairState State { get; }
|
||||
public bool Initialized { get; }
|
||||
void Kill();
|
||||
List<string> TestHistory { get; }
|
||||
PairSettings Settings { get; set; }
|
||||
|
||||
int ServerSeed { get; }
|
||||
int ClientSeed { get; }
|
||||
|
||||
void ActivateContext(TextWriter testOut);
|
||||
void ValidateSettings(PairSettings settings);
|
||||
void SetupSeed();
|
||||
void ClearModifiedCvars();
|
||||
void Use();
|
||||
Task Init(int id, BasePoolManager manager, PairSettings settings, TextWriter testOut);
|
||||
Task RecycleInternal(PairSettings next, TextWriter testOut);
|
||||
Task ApplySettings(PairSettings settings);
|
||||
Task RunTicksSync(int ticks);
|
||||
Task SyncTicks(int targetDelta = 1);
|
||||
}
|
||||
|
||||
public enum PairState : byte
|
||||
{
|
||||
Ready = 0,
|
||||
InUse = 1,
|
||||
CleanDisposed = 2,
|
||||
Dead = 3,
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for a server-client pair. These settings may change over a pair's lifetime.
|
||||
/// The pool manager handles fetching pairs with a given setting, including applying new settings to re-used pairs.
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class PairSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Set to true if the test will ruin the server/client pair.
|
||||
/// </summary>
|
||||
public virtual bool Destructive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be created fresh.
|
||||
/// </summary>
|
||||
public virtual bool Fresh { 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.
|
||||
/// </summary>
|
||||
public virtual bool Connected { 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="RobustTestPair.IsTestPrototype(EntityPrototype)"/> if you need to
|
||||
/// exclude test prototypes.
|
||||
/// </summary>
|
||||
public virtual bool NoLoadTestPrototypes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true to disable the NetInterp CVar on the given server/client pair
|
||||
/// </summary>
|
||||
public virtual 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 virtual bool Dirty { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the test name detection, and uses this in the test history instead
|
||||
/// </summary>
|
||||
public virtual string? TestName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public virtual int? ServerSeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public virtual int? ClientSeed { get; set; }
|
||||
|
||||
#region Inferred Properties
|
||||
|
||||
/// <summary>
|
||||
/// If the returned pair must not be reused
|
||||
/// </summary>
|
||||
public virtual bool MustNotBeReused => Destructive || NoLoadTestPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// If the given pair must be brand new
|
||||
/// </summary>
|
||||
public virtual bool MustBeNew => Fresh || NoLoadTestPrototypes;
|
||||
|
||||
#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 virtual bool CanFastRecycle(PairSettings nextSettings)
|
||||
{
|
||||
if (MustNotBeReused)
|
||||
throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
|
||||
|
||||
if (nextSettings.MustBeNew)
|
||||
throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
|
||||
|
||||
if (Dirty)
|
||||
return false;
|
||||
|
||||
return Connected == nextSettings.Connected;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
public abstract class BasePoolManager
|
||||
{
|
||||
internal abstract void Return(ITestPair pair);
|
||||
public abstract Assembly[] ClientAssemblies { get; }
|
||||
public abstract Assembly[] ServerAssemblies { get; }
|
||||
public readonly List<string> TestPrototypes = new();
|
||||
|
||||
// default cvar overrides to use when creating test pairs.
|
||||
public readonly List<(string cvar, string value)> DefaultCvars =
|
||||
[
|
||||
(CVars.NetPVS.Name, "false"),
|
||||
(CVars.ThreadParallelCount.Name, "1"),
|
||||
(CVars.ReplayClientRecordingEnabled.Name, "false"),
|
||||
(CVars.ReplayServerRecordingEnabled.Name, "false"),
|
||||
(CVars.NetBufferSize.Name, "0")
|
||||
];
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class PoolManager<TPair> : BasePoolManager where TPair : class, ITestPair, new()
|
||||
{
|
||||
private int _nextPairId;
|
||||
private readonly Lock _pairLock = new();
|
||||
private bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Set of all pairs, and whether they are currently in-use
|
||||
/// </summary>
|
||||
protected readonly Dictionary<TPair, bool> Pairs = new();
|
||||
private bool _dead;
|
||||
private Exception? _poolFailureReason;
|
||||
|
||||
private Assembly[] _clientAssemblies = [];
|
||||
private Assembly[] _serverAssemblies = [];
|
||||
|
||||
public override Assembly[] ClientAssemblies => _clientAssemblies;
|
||||
public override Assembly[] ServerAssemblies => _serverAssemblies;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the pool manager. Override this to configure what assemblies should get loaded.
|
||||
/// </summary>
|
||||
public virtual void Startup(params Assembly[] extraAssemblies)
|
||||
{
|
||||
// By default, load no content assemblies, but make both server & client load the testing assembly.
|
||||
Startup([], [], extraAssemblies);
|
||||
}
|
||||
|
||||
protected void Startup(Assembly[] clientAssemblies, Assembly[] serverAssemblies, Assembly[] sharedAssemblies)
|
||||
{
|
||||
if (_initialized)
|
||||
throw new InvalidOperationException("Already initialized");
|
||||
|
||||
DebugTools.AssertEqual(clientAssemblies.Intersect(sharedAssemblies).Count(), 0);
|
||||
DebugTools.AssertEqual(serverAssemblies.Intersect(sharedAssemblies).Count(), 0);
|
||||
DebugTools.AssertEqual(serverAssemblies.Intersect(clientAssemblies).Count(), 0);
|
||||
|
||||
foreach (var assembly in sharedAssemblies)
|
||||
{
|
||||
DiscoverTestPrototypes(assembly);
|
||||
}
|
||||
|
||||
foreach (var assembly in clientAssemblies)
|
||||
{
|
||||
DiscoverTestPrototypes(assembly);
|
||||
}
|
||||
|
||||
foreach (var assembly in serverAssemblies)
|
||||
{
|
||||
DiscoverTestPrototypes(assembly);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
_clientAssemblies = clientAssemblies.Concat(sharedAssemblies).ToArray();
|
||||
_serverAssemblies = serverAssemblies.Concat(sharedAssemblies).ToArray();
|
||||
}
|
||||
|
||||
/// <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 void Shutdown()
|
||||
{
|
||||
List<TPair> localPairs;
|
||||
lock (_pairLock)
|
||||
{
|
||||
if (_dead)
|
||||
return;
|
||||
_dead = true;
|
||||
localPairs = Pairs.Keys.ToList();
|
||||
}
|
||||
|
||||
foreach (var pair in localPairs)
|
||||
{
|
||||
pair.Kill();
|
||||
}
|
||||
|
||||
_initialized = false;
|
||||
TestPrototypes.Clear();
|
||||
}
|
||||
|
||||
protected virtual string GetDefaultTestName(TestContext testContext)
|
||||
{
|
||||
return testContext.Test.FullName.Replace("Robust.UnitTesting.", "");
|
||||
}
|
||||
|
||||
public 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 virtual PairSettings DefaultSettings => new();
|
||||
|
||||
public async Task<TPair> GetPair(PairSettings? settings = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
throw new InvalidOperationException($"Pool manager has not been initialized");
|
||||
|
||||
settings ??= DefaultSettings;
|
||||
|
||||
// Trust issues with the AsyncLocal that backs this.
|
||||
var testContext = TestContext.CurrentContext;
|
||||
var testOut = TestContext.Out;
|
||||
|
||||
DieIfPoolFailure();
|
||||
var currentTestName = settings.TestName ?? GetDefaultTestName(testContext);
|
||||
var watch = new Stopwatch();
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Called by test {currentTestName}");
|
||||
TPair? pair = null;
|
||||
try
|
||||
{
|
||||
watch.Start();
|
||||
if (settings.MustBeNew)
|
||||
{
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetPair)}: Creating pair, because settings of pool settings");
|
||||
pair = await CreateServerClientPair(settings, testOut);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Looking in pool for a suitable pair");
|
||||
pair = GrabOptimalPair(settings);
|
||||
if (pair != null)
|
||||
{
|
||||
pair.ActivateContext(testOut);
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Suitable pair found");
|
||||
|
||||
if (pair.Settings.CanFastRecycle(settings))
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Cleanup not needed, Skipping cleanup of pair");
|
||||
await pair.ApplySettings(settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Cleaning existing pair");
|
||||
await pair.RecycleInternal(settings, testOut);
|
||||
}
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.SyncTicks(targetDelta: 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Creating a new pair, no suitable pair found in pool");
|
||||
pair = await CreateServerClientPair(settings, testOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pair != null && pair.TestHistory.Count > 0)
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: 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(GetPair)}: Pair {pair.Id} Test History End");
|
||||
}
|
||||
}
|
||||
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Retrieving pair {pair.Id} from pool took {watch.Elapsed.TotalMilliseconds} ms");
|
||||
|
||||
pair.ValidateSettings(settings);
|
||||
pair.ClearModifiedCvars();
|
||||
pair.Settings = settings;
|
||||
pair.TestHistory.Add(currentTestName);
|
||||
pair.SetupSeed();
|
||||
|
||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
|
||||
|
||||
pair.Watch.Restart();
|
||||
return pair;
|
||||
}
|
||||
|
||||
private TPair? GrabOptimalPair(PairSettings poolSettings)
|
||||
{
|
||||
lock (_pairLock)
|
||||
{
|
||||
TPair? 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)
|
||||
return null;
|
||||
|
||||
fallback.Use();
|
||||
Pairs[fallback!] = true;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by TestPair after checking the server/client pair, Don't use this.
|
||||
/// </summary>
|
||||
internal override void Return(ITestPair pair)
|
||||
{
|
||||
lock (_pairLock)
|
||||
{
|
||||
if (pair.State == PairState.Dead)
|
||||
Pairs.Remove((TPair)pair);
|
||||
else if (pair.State == PairState.Ready)
|
||||
Pairs[(TPair) pair] = false;
|
||||
else
|
||||
throw new InvalidOperationException($"Attempted to return a pair in an invalid state. Pair: {pair.Id}. State: {pair.State}.");
|
||||
}
|
||||
}
|
||||
|
||||
private 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 async Task<TPair> CreateServerClientPair(PairSettings settings, TextWriter testOut)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextPairId);
|
||||
var pair = new TPair();
|
||||
await pair.Init(id, this, settings, testOut);
|
||||
pair.Use();
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.SyncTicks(targetDelta: 1);
|
||||
return pair;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_poolFailureReason = ex;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void DiscoverTestPrototypes(Assembly assembly)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Static
|
||||
| BindingFlags.NonPublic
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.DeclaredOnly;
|
||||
|
||||
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($"{nameof(TestPrototypesAttribute)} is only valid on non-null string fields");
|
||||
|
||||
TestPrototypes.Add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
/// <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; }
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
// This partial file contains misc helper functions to make writing tests easier.
|
||||
public partial class TestPair<TServer, TClient>
|
||||
{
|
||||
/// <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, IIntegrationInstance source, IIntegrationInstance 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.Resolve<IComponentFactory>().TryGetRegistration<T>(out var reg)
|
||||
&& !Client.Resolve<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)
|
||||
{
|
||||
if (!Server.Resolve<IComponentFactory>().TryGetRegistration(type, out var reg)
|
||||
&& !Client.Resolve<IComponentFactory>().TryGetRegistration(type, out reg))
|
||||
{
|
||||
Assert.Fail($"Unknown component: {type.Name}");
|
||||
return new();
|
||||
}
|
||||
|
||||
var id = reg.Name;
|
||||
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 async Task Connect()
|
||||
{
|
||||
Assert.That(Client.NetMan.IsConnected, Is.False);
|
||||
await Client.Connect(Server);
|
||||
await ReallyBeIdle(10);
|
||||
await Client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
public async Task Disconnect(string reason = "")
|
||||
{
|
||||
await Client.WaitPost(() => Client.CNetMan.ClientDisconnect(reason));
|
||||
await ReallyBeIdle(10);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype(EntityPrototype proto)
|
||||
{
|
||||
return _loadedEntityPrototypes.Contains(proto.ID);
|
||||
}
|
||||
|
||||
public bool IsTestEntityPrototype(string id)
|
||||
{
|
||||
return _loadedEntityPrototypes.Contains(id);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype<TPrototype>(string id) where TPrototype : IPrototype
|
||||
{
|
||||
return IsTestPrototype(typeof(TPrototype), id);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype<TPrototype>(TPrototype proto) where TPrototype : IPrototype
|
||||
{
|
||||
return IsTestPrototype(typeof(TPrototype), proto.ID);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype(Type kind, string id)
|
||||
{
|
||||
return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a map with a single grid consisting of one tile.
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(TestMap))]
|
||||
public async Task<TestMapData> CreateTestMap(bool initialized, ushort tileTypeId)
|
||||
{
|
||||
TestMap = new TestMapData();
|
||||
await Server.WaitIdleAsync();
|
||||
var sys = Server.System<SharedMapSystem>();
|
||||
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
TestMap.MapUid = sys.CreateMap(out TestMap.MapId, runMapInit: initialized);
|
||||
TestMap.Grid = Server.MapMan.CreateGridEntity(TestMap.MapId);
|
||||
TestMap.GridCoords = new EntityCoordinates(TestMap.Grid, 0, 0);
|
||||
TestMap.MapCoords = new MapCoordinates(0, 0, TestMap.MapId);
|
||||
sys.SetTile(TestMap.Grid.Owner, TestMap.Grid.Comp, TestMap.GridCoords, new Tile(tileTypeId));
|
||||
TestMap.Tile = sys.GetAllTiles(TestMap.Grid.Owner, TestMap.Grid.Comp).First();
|
||||
});
|
||||
|
||||
if (!Settings.Connected)
|
||||
return TestMap;
|
||||
|
||||
await RunTicksSync(10);
|
||||
TestMap.CMapUid = ToClientUid(TestMap.MapUid);
|
||||
TestMap.CGridUid = ToClientUid(TestMap.Grid);
|
||||
TestMap.CGridCoords = new EntityCoordinates(TestMap.CGridUid, 0, 0);
|
||||
|
||||
return TestMap;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CreateTestMap(bool, ushort)"/>
|
||||
[MemberNotNull(nameof(TestMap))]
|
||||
public async Task<TestMapData> CreateTestMap(bool initialized, string tileName)
|
||||
{
|
||||
var defMan = Server.Resolve<ITileDefinitionManager>();
|
||||
if (!defMan.TryGetDefinition(tileName, out var def))
|
||||
Assert.Fail($"Unknown tile: {tileName}");
|
||||
return await CreateTestMap(initialized, def?.TileId ?? 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
// This partial file contains logic related to recycling & disposing test pairs.
|
||||
public partial class TestPair<TServer, TClient>
|
||||
{
|
||||
private async Task OnDirtyDispose()
|
||||
{
|
||||
var usageTime = Watch.Elapsed;
|
||||
Watch.Restart();
|
||||
await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
|
||||
Kill();
|
||||
var disposeTime = Watch.Elapsed;
|
||||
await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
|
||||
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
|
||||
// because someone forgot to clean-return the pair.
|
||||
Assert.Warn("Test was dirty-disposed.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets called before any test pair gets returned to the pool.
|
||||
/// </summary>
|
||||
protected virtual async Task Cleanup()
|
||||
{
|
||||
if (TestMap != null)
|
||||
{
|
||||
await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
|
||||
TestMap = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCleanDispose()
|
||||
{
|
||||
await Server.WaitIdleAsync();
|
||||
await Client.WaitIdleAsync();
|
||||
await Cleanup();
|
||||
await Server.Cleanup();
|
||||
await Client.Cleanup();
|
||||
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.Resolve<IRuntimeLog>();
|
||||
if (sRuntimeLog.ExceptionCount > 0)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
|
||||
var cRuntimeLog = Client.Resolve<IRuntimeLog>();
|
||||
if (cRuntimeLog.ExceptionCount > 0)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
|
||||
|
||||
var returnTime = Watch.Elapsed;
|
||||
await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
|
||||
}
|
||||
|
||||
public async ValueTask CleanReturnAsync()
|
||||
{
|
||||
if (State != PairState.InUse)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
||||
|
||||
await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
|
||||
State = PairState.CleanDisposed;
|
||||
await OnCleanDispose();
|
||||
State = PairState.Ready;
|
||||
Manager.Return(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();
|
||||
Manager.Return(this);
|
||||
ClearContext();
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets called when a previously used test pair is being retrieved from the pool.
|
||||
/// Note that in some instances this method may get skipped (See <see cref="PairSettings.CanFastRecycle"/>).
|
||||
/// </summary>
|
||||
public async Task RecycleInternal(PairSettings settings, TextWriter testOut)
|
||||
{
|
||||
Watch.Restart();
|
||||
await testOut.WriteLineAsync($"Recycling...");
|
||||
await RunTicksSync(1);
|
||||
|
||||
// Disconnect the client if they are connected.
|
||||
if (Client.CNetMan.IsConnected)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
|
||||
await Client.WaitPost(() => Client.CNetMan.ClientDisconnect("Test pooling cleanup disconnect"));
|
||||
await RunTicksSync(1);
|
||||
}
|
||||
|
||||
await Recycle(settings, testOut);
|
||||
ClearModifiedCvars();
|
||||
|
||||
// (possibly) reconnect the client
|
||||
if (settings.Connected)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
|
||||
await Client.Connect(Server);
|
||||
}
|
||||
|
||||
Settings = default!;
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
|
||||
await ReallyBeIdle();
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets called when a previously used test pair is being retrieved from the pool.
|
||||
/// If the next settings are compatible with the previous settings, this step may get skipped (See <see cref="PairSettings.CanFastRecycle"/>).
|
||||
/// In general, this method should also call <see cref="ApplySettings"/>.
|
||||
/// </summary>
|
||||
protected virtual async Task Recycle(PairSettings next, TextWriter testOut)
|
||||
{
|
||||
//Apply Cvars
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
|
||||
await ApplySettings(next);
|
||||
await RunTicksSync(1);
|
||||
|
||||
// flush server entities.
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Flushing server entities");
|
||||
await Server.WaitPost(() => Server.EntMan.FlushEntities());
|
||||
await RunTicksSync(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply settings to the test pair. This method is always called when a pair is fetched from the pool. There should
|
||||
/// be no need to apply settings that require a pair to be recycled, as in those cases the
|
||||
/// <see cref="PairSettings.CanFastRecycle"/> should have caused <see cref="Recycle"/> to be invoked, which should
|
||||
/// already have applied those settings.
|
||||
/// </summary>
|
||||
public async Task ApplySettings(PairSettings next)
|
||||
{
|
||||
await ApplySettings(Client, next);
|
||||
await ApplySettings(Server, next);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ApplySettings(PairSettings)"/>
|
||||
[MustCallBase]
|
||||
protected internal virtual async Task ApplySettings(IIntegrationInstance instance, PairSettings next)
|
||||
{
|
||||
if (instance.CfgMan.IsCVarRegistered(CVars.NetInterp.Name))
|
||||
await instance.WaitPost(() => instance.CfgMan.SetCVar(CVars.NetInterp, !next.DisableInterpolate));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after a test pair has been recycled to validate that the settings have been properly applied.
|
||||
/// </summary>
|
||||
[MustCallBase]
|
||||
public virtual void ValidateSettings(PairSettings settings)
|
||||
{
|
||||
var netMan = Client.Resolve<INetManager>();
|
||||
Assert.That(netMan.IsConnected, Is.EqualTo(settings.Connected));
|
||||
|
||||
if (!settings.Connected)
|
||||
return;
|
||||
|
||||
var baseClient = Client.Resolve<IBaseClient>();
|
||||
var cPlayer = Client.Resolve<ISharedPlayerManager>();
|
||||
var sPlayer = Server.Resolve<ISharedPlayerManager>();
|
||||
|
||||
Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
|
||||
Assert.That(sPlayer.Sessions.Length, Is.EqualTo(1));
|
||||
var session = sPlayer.Sessions.Single();
|
||||
Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
/// <summary>
|
||||
/// This object wraps a pooled server+client pair.
|
||||
/// </summary>
|
||||
public abstract partial class TestPair<TServer, TClient> : ITestPair, IAsyncDisposable
|
||||
where TServer : IServerIntegrationInstance
|
||||
where TClient : IClientIntegrationInstance
|
||||
{
|
||||
public int Id { get; internal set; }
|
||||
protected BasePoolManager Manager = default!;
|
||||
public PairState State { get; private set; } = PairState.Ready;
|
||||
public bool Initialized { get; private set; }
|
||||
protected TextWriter TestOut = default!;
|
||||
public Stopwatch Watch { get; } = new();
|
||||
public List<string> TestHistory { get; } = new();
|
||||
public PairSettings Settings { get; set; } = default!;
|
||||
|
||||
public readonly PoolTestLogHandler ServerLogHandler = new("SERVER");
|
||||
public readonly PoolTestLogHandler ClientLogHandler = new("CLIENT");
|
||||
public TestMapData? TestMap;
|
||||
|
||||
private int _nextServerSeed;
|
||||
private int _nextClientSeed;
|
||||
|
||||
public int ServerSeed { get; set; }
|
||||
public int ClientSeed { get; set; }
|
||||
|
||||
public TServer Server { get; private set; } = default!;
|
||||
public TClient Client { get; private set; } = default!;
|
||||
|
||||
public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User ?? default);
|
||||
|
||||
private Dictionary<Type, HashSet<string>> _loadedPrototypes = new();
|
||||
private HashSet<string> _loadedEntityPrototypes = new();
|
||||
protected readonly Dictionary<string, object> ModifiedClientCvars = new();
|
||||
protected readonly Dictionary<string, object> ModifiedServerCvars = new();
|
||||
|
||||
public async Task LoadPrototypes(List<string> prototypes)
|
||||
{
|
||||
await LoadPrototypes(Server, prototypes);
|
||||
await LoadPrototypes(Client, prototypes);
|
||||
}
|
||||
|
||||
public async Task Init(
|
||||
int id,
|
||||
BasePoolManager manager,
|
||||
PairSettings settings,
|
||||
TextWriter testOut)
|
||||
{
|
||||
if (Initialized)
|
||||
throw new InvalidOperationException("Already initialized");
|
||||
|
||||
Id = id;
|
||||
Manager = manager;
|
||||
Settings = settings;
|
||||
Initialized = true;
|
||||
|
||||
ClientLogHandler.ActivateContext(testOut);
|
||||
ServerLogHandler.ActivateContext(testOut);
|
||||
Client = await GenerateClient();
|
||||
Server = await GenerateServer();
|
||||
ActivateContext(testOut);
|
||||
await ApplySettings(settings);
|
||||
|
||||
Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
|
||||
Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
|
||||
|
||||
if (!settings.NoLoadTestPrototypes)
|
||||
await LoadPrototypes(Manager.TestPrototypes);
|
||||
|
||||
var cRand = Client.Resolve<IRobustRandom>();
|
||||
var sRand = Server.Resolve<IRobustRandom>();
|
||||
_nextClientSeed = cRand.Next();
|
||||
_nextServerSeed = sRand.Next();
|
||||
|
||||
await Initialize();
|
||||
|
||||
// Always initially connect clients.
|
||||
// This is done in case the server does randomization when client first connects
|
||||
// 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 the initial configuration from being randomized.
|
||||
await Connect();
|
||||
|
||||
if (!Settings.Connected)
|
||||
await Disconnect("Initial disconnect");
|
||||
}
|
||||
|
||||
protected virtual Task Initialize()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected abstract Task<TClient> GenerateClient();
|
||||
protected abstract Task<TServer> GenerateServer();
|
||||
|
||||
public void Kill()
|
||||
{
|
||||
State = PairState.Dead;
|
||||
ServerLogHandler.ShuttingDown = true;
|
||||
ClientLogHandler.ShuttingDown = true;
|
||||
Server.Dispose();
|
||||
Client.Dispose();
|
||||
}
|
||||
|
||||
private void ClearContext()
|
||||
{
|
||||
TestOut = default!;
|
||||
ServerLogHandler.ClearContext();
|
||||
ClientLogHandler.ClearContext();
|
||||
}
|
||||
|
||||
public void ActivateContext(TextWriter testOut)
|
||||
{
|
||||
TestOut = testOut;
|
||||
ServerLogHandler.ActivateContext(testOut);
|
||||
ClientLogHandler.ActivateContext(testOut);
|
||||
}
|
||||
|
||||
public void Use()
|
||||
{
|
||||
if (State != PairState.Ready)
|
||||
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
|
||||
State = PairState.InUse;
|
||||
}
|
||||
|
||||
public void SetupSeed()
|
||||
{
|
||||
var sRand = Server.Resolve<IRobustRandom>();
|
||||
if (Settings.ServerSeed is { } severSeed)
|
||||
{
|
||||
ServerSeed = severSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerSeed = _nextServerSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
_nextServerSeed = sRand.Next();
|
||||
}
|
||||
|
||||
var cRand = Client.Resolve<IRobustRandom>();
|
||||
if (Settings.ClientSeed is { } clientSeed)
|
||||
{
|
||||
ClientSeed = clientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClientSeed = _nextClientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
_nextClientSeed = cRand.Next();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPrototypes(IIntegrationInstance 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 void Deconstruct(out TServer server, out TClient client)
|
||||
{
|
||||
server = Server;
|
||||
client = Client;
|
||||
}
|
||||
|
||||
private void OnServerCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
ModifiedServerCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
private void OnClientCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
ModifiedClientCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
public void ClearModifiedCvars()
|
||||
{
|
||||
ModifiedClientCvars.Clear();
|
||||
ModifiedServerCvars.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts any cvars that were modified during a test back to their original values.
|
||||
/// </summary>
|
||||
public virtual 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);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.UnitTesting.Pool;
|
||||
|
||||
/// <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
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user