mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01: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:
@@ -130,6 +130,7 @@ END TEMPLATE-->
|
||||
|
||||
* Add CC ND licences to the RGA validator.
|
||||
* Add entity spawn prediction and entity deletion prediction. This is currently limited as you are unable to predict interactions with these entities. These are done via the new methods prefixed with "Predicted". You can also manually flag an entity as a predicted spawn with the `FlagPredicted` method which will clean it up when prediction is reset.
|
||||
* Added `PoolManager` & `TestPair` classes to `Robust.UnitTesting`. These classes make it easier to create & use pooled server/client instance pairs in integration tests.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
|
||||
118
Robust.UnitTesting/IIntegrationInstance.cs
Normal file
118
Robust.UnitTesting/IIntegrationInstance.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.UnitTesting;
|
||||
|
||||
public interface IIntegrationInstance : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the instance is still alive.
|
||||
/// "Alive" indicates that it is able to receive and process commands.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if you did not ensure that the instance is idle via <see cref="WaitIdleAsync"/> first.
|
||||
/// </exception>
|
||||
bool IsAlive { get; }
|
||||
|
||||
Exception? UnhandledException { get; }
|
||||
|
||||
EntityManager EntMan { get; }
|
||||
IPrototypeManager ProtoMan { get; }
|
||||
IConfigurationManager CfgMan { get; }
|
||||
ISharedPlayerManager PlayerMan { get; }
|
||||
INetManager NetMan { get; }
|
||||
IMapManager MapMan { get; }
|
||||
IGameTiming Timing { get; }
|
||||
ISawmill Log { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a dependency inside the instance.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if you did not ensure that the instance is idle via <see cref="WaitIdleAsync"/> first.
|
||||
/// </exception>
|
||||
[Pure] T Resolve<T>();
|
||||
|
||||
[Pure] T System<T>() where T : IEntitySystem;
|
||||
|
||||
TransformComponent Transform(EntityUid uid);
|
||||
MetaDataComponent MetaData(EntityUid uid);
|
||||
MetaDataComponent MetaData(NetEntity uid);
|
||||
TransformComponent Transform(NetEntity uid);
|
||||
|
||||
Task ExecuteCommand(string cmd);
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the instance to go idle, either through finishing all commands or shutting down/crashing.
|
||||
/// </summary>
|
||||
/// <param name="throwOnUnhandled">
|
||||
/// If true, throw an exception if the server dies on an unhandled exception.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="Exception">
|
||||
/// Thrown if <paramref name="throwOnUnhandled"/> is true and the instance shuts down on an unhandled exception.
|
||||
/// </exception>
|
||||
Task WaitIdleAsync(bool throwOnUnhandled = true, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Queue for the server to run n ticks.
|
||||
/// </summary>
|
||||
/// <param name="ticks">The amount of ticks to run.</param>
|
||||
void RunTicks(int ticks);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunTicks"/> followed by <see cref="WaitIdleAsync"/>
|
||||
/// </summary>
|
||||
Task WaitRunTicks(int ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Queue for a delegate to be ran inside the main loop of the instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not run NUnit assertions inside <see cref="Post"/>. Use <see cref="Assert"/> instead.
|
||||
/// </remarks>
|
||||
void Post(Action post);
|
||||
|
||||
/// <inheritdoc cref="Post"/>
|
||||
Task WaitPost(Action post);
|
||||
|
||||
/// <summary>
|
||||
/// Queue for a delegate to be ran inside the main loop of the instance,
|
||||
/// rethrowing any exceptions in <see cref="WaitIdleAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Exceptions raised inside this callback will be rethrown by <see cref="WaitIdleAsync"/>.
|
||||
/// This makes it ideal for NUnit assertions,
|
||||
/// since rethrowing the NUnit assertion directly provides less noise.
|
||||
/// </remarks>
|
||||
void Assert(Action assertion);
|
||||
|
||||
/// <inheritdoc cref="Assert"/>
|
||||
Task WaitAssertion(Action assertion);
|
||||
|
||||
/// <summary>
|
||||
/// Post-test cleanup
|
||||
/// </summary>
|
||||
Task Cleanup();
|
||||
}
|
||||
|
||||
public interface IClientIntegrationInstance : IIntegrationInstance
|
||||
{
|
||||
IClientNetManager CNetMan => (IClientNetManager) NetMan;
|
||||
ICommonSession? Session { get; }
|
||||
NetUserId? User { get; }
|
||||
EntityUid? AttachedEntity { get; }
|
||||
Task Connect(IServerIntegrationInstance target);
|
||||
}
|
||||
|
||||
public interface IServerIntegrationInstance : IIntegrationInstance;
|
||||
39
Robust.UnitTesting/Pool/ITestPair.cs
Normal file
39
Robust.UnitTesting/Pool/ITestPair.cs
Normal file
@@ -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,
|
||||
}
|
||||
95
Robust.UnitTesting/Pool/PairSettings.cs
Normal file
95
Robust.UnitTesting/Pool/PairSettings.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
332
Robust.UnitTesting/Pool/PoolManager.cs
Normal file
332
Robust.UnitTesting/Pool/PoolManager.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Robust.UnitTesting/Pool/PoolTestLogHandler.cs
Normal file
77
Robust.UnitTesting/Pool/PoolTestLogHandler.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
23
Robust.UnitTesting/Pool/TestMapData.cs
Normal file
23
Robust.UnitTesting/Pool/TestMapData.cs
Normal file
@@ -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; }
|
||||
}
|
||||
285
Robust.UnitTesting/Pool/TestPair.Helpers.cs
Normal file
285
Robust.UnitTesting/Pool/TestPair.Helpers.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
208
Robust.UnitTesting/Pool/TestPair.Recycle.cs
Normal file
208
Robust.UnitTesting/Pool/TestPair.Recycle.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
244
Robust.UnitTesting/Pool/TestPair.cs
Normal file
244
Robust.UnitTesting/Pool/TestPair.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
13
Robust.UnitTesting/Pool/TestPrototypesAttribute.cs
Normal file
13
Robust.UnitTesting/Pool/TestPrototypesAttribute.cs
Normal file
@@ -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
|
||||
{
|
||||
}
|
||||
80
Robust.UnitTesting/RobustIntegrationTest.TestPair.cs
Normal file
80
Robust.UnitTesting/RobustIntegrationTest.TestPair.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client;
|
||||
using Robust.Server;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.UnitTesting.Pool;
|
||||
|
||||
namespace Robust.UnitTesting;
|
||||
|
||||
public partial class RobustIntegrationTest
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="TestPair{TServer,TClient}"/> implementation using <see cref="RobustIntegrationTest"/> instances.
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class TestPair : TestPair<ServerIntegrationInstance, ClientIntegrationInstance>
|
||||
{
|
||||
protected override async Task<ClientIntegrationInstance> GenerateClient()
|
||||
{
|
||||
var client = new ClientIntegrationInstance(ClientOptions());
|
||||
await client.WaitIdleAsync();
|
||||
client.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
|
||||
client.CfgMan.OnValueChanged(RTCVars.FailureLogLevel, value => ClientLogHandler.FailureLevel = value, true);
|
||||
await client.WaitIdleAsync();
|
||||
return client;
|
||||
}
|
||||
|
||||
protected override async Task<ServerIntegrationInstance> GenerateServer()
|
||||
{
|
||||
var server = new ServerIntegrationInstance(ServerOptions());
|
||||
await server.WaitIdleAsync();
|
||||
server.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
|
||||
server.CfgMan.OnValueChanged(RTCVars.FailureLogLevel, value => ServerLogHandler.FailureLevel = value, true);
|
||||
return server;
|
||||
}
|
||||
|
||||
protected virtual ClientIntegrationOptions ClientOptions()
|
||||
{
|
||||
var options = new ClientIntegrationOptions
|
||||
{
|
||||
ContentAssemblies = Manager.ClientAssemblies,
|
||||
OverrideLogHandler = () => ClientLogHandler
|
||||
};
|
||||
|
||||
options.Options = new()
|
||||
{
|
||||
LoadConfigAndUserData = false,
|
||||
LoadContentResources = false,
|
||||
};
|
||||
|
||||
foreach (var (cvar, value) in Manager.DefaultCvars)
|
||||
{
|
||||
options.CVarOverrides[cvar] = value;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
protected virtual ServerIntegrationOptions ServerOptions()
|
||||
{
|
||||
var options = new ServerIntegrationOptions
|
||||
{
|
||||
ContentAssemblies = Manager.ServerAssemblies,
|
||||
OverrideLogHandler = () => ServerLogHandler
|
||||
};
|
||||
|
||||
options.Options = new()
|
||||
{
|
||||
LoadConfigAndUserData = false,
|
||||
LoadContentResources = false,
|
||||
};
|
||||
|
||||
foreach (var (cvar, value) in Manager.DefaultCvars)
|
||||
{
|
||||
options.CVarOverrides[cvar] = value;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,6 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML.Proxy;
|
||||
@@ -33,13 +31,13 @@ using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.UnitTesting.Pool;
|
||||
using ServerProgram = Robust.Server.Program;
|
||||
|
||||
namespace Robust.UnitTesting
|
||||
@@ -281,7 +279,7 @@ namespace Robust.UnitTesting
|
||||
/// This method must be used before trying to access any state like <see cref="ResolveDependency{T}"/>,
|
||||
/// to prevent race conditions.
|
||||
/// </remarks>
|
||||
public abstract class IntegrationInstance : IDisposable
|
||||
public abstract class IntegrationInstance : IIntegrationInstance
|
||||
{
|
||||
private protected Thread? InstanceThread;
|
||||
private protected IDependencyCollection DependencyCollection = default!;
|
||||
@@ -305,10 +303,11 @@ namespace Robust.UnitTesting
|
||||
|
||||
public virtual IntegrationOptions? Options { get; internal set; }
|
||||
|
||||
public IEntityManager EntMan { get; private set; } = default!;
|
||||
public EntityManager EntMan { get; private set; } = default!;
|
||||
public IPrototypeManager ProtoMan { get; private set; } = default!;
|
||||
public IConfigurationManager CfgMan { get; private set; } = default!;
|
||||
public ISharedPlayerManager PlayerMan { get; private set; } = default!;
|
||||
public INetManager NetMan { get; private set; } = default!;
|
||||
public IGameTiming Timing { get; private set; } = default!;
|
||||
public IMapManager MapMan { get; private set; } = default!;
|
||||
public IConsoleHost ConsoleHost { get; private set; } = default!;
|
||||
@@ -316,21 +315,26 @@ namespace Robust.UnitTesting
|
||||
|
||||
protected virtual void ResolveIoC(IDependencyCollection deps)
|
||||
{
|
||||
EntMan = deps.Resolve<IEntityManager>();
|
||||
EntMan = deps.Resolve<EntityManager>();
|
||||
ProtoMan = deps.Resolve<IPrototypeManager>();
|
||||
CfgMan = deps.Resolve<IConfigurationManager>();
|
||||
PlayerMan = deps.Resolve<ISharedPlayerManager>();
|
||||
Timing = deps.Resolve<IGameTiming>();
|
||||
NetMan = deps.Resolve<INetManager>();
|
||||
MapMan = deps.Resolve<IMapManager>();
|
||||
ConsoleHost = deps.Resolve<IConsoleHost>();
|
||||
Log = deps.Resolve<ILogManager>().GetSawmill("test");
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public T System<T>() where T : IEntitySystem
|
||||
{
|
||||
return EntMan.System<T>();
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public T Resolve<T>() => ResolveDependency<T>();
|
||||
|
||||
public TransformComponent Transform(EntityUid uid)
|
||||
{
|
||||
return EntMan.GetComponent<TransformComponent>(uid);
|
||||
@@ -352,13 +356,7 @@ namespace Robust.UnitTesting
|
||||
await WaitPost(() => ConsoleHost.ExecuteCommand(cmd));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the instance is still alive.
|
||||
/// "Alive" indicates that it is able to receive and process commands.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if you did not ensure that the instance is idle via <see cref="WaitIdleAsync"/> first.
|
||||
/// </exception>
|
||||
/// <inheritdoc/>
|
||||
public bool IsAlive
|
||||
{
|
||||
get
|
||||
@@ -438,16 +436,7 @@ namespace Robust.UnitTesting
|
||||
return DependencyCollection.Resolve<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the instance to go idle, either through finishing all commands or shutting down/crashing.
|
||||
/// </summary>
|
||||
/// <param name="throwOnUnhandled">
|
||||
/// If true, throw an exception if the server dies on an unhandled exception.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="Exception">
|
||||
/// Thrown if <paramref name="throwOnUnhandled"/> is true and the instance shuts down on an unhandled exception.
|
||||
/// </exception>
|
||||
/// <inheritdoc/>
|
||||
public Task WaitIdleAsync(bool throwOnUnhandled = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Options?.Asynchronous != false)
|
||||
@@ -558,10 +547,7 @@ namespace Robust.UnitTesting
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue for the server to run n ticks.
|
||||
/// </summary>
|
||||
/// <param name="ticks">The amount of ticks to run.</param>
|
||||
/// <inheritdoc/>
|
||||
public void RunTicks(int ticks)
|
||||
{
|
||||
_isSurelyIdle = false;
|
||||
@@ -569,9 +555,7 @@ namespace Robust.UnitTesting
|
||||
_toInstanceWriter.TryWrite(new RunTicksMessage(ticks, _currentTicksId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunTicks"/> followed by <see cref="WaitIdleAsync"/>
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public async Task WaitRunTicks(int ticks)
|
||||
{
|
||||
RunTicks(ticks);
|
||||
@@ -590,12 +574,7 @@ namespace Robust.UnitTesting
|
||||
_toInstanceWriter.TryComplete();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue for a delegate to be ran inside the main loop of the instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not run NUnit assertions inside <see cref="Post"/>. Use <see cref="Assert"/> instead.
|
||||
/// </remarks>
|
||||
/// <inheritdoc/>
|
||||
public void Post(Action post)
|
||||
{
|
||||
_isSurelyIdle = false;
|
||||
@@ -609,15 +588,7 @@ namespace Robust.UnitTesting
|
||||
await WaitIdleAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue for a delegate to be ran inside the main loop of the instance,
|
||||
/// rethrowing any exceptions in <see cref="WaitIdleAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Exceptions raised inside this callback will be rethrown by <see cref="WaitIdleAsync"/>.
|
||||
/// This makes it ideal for NUnit assertions,
|
||||
/// since rethrowing the NUnit assertion directly provides less noise.
|
||||
/// </remarks>
|
||||
/// <inheritdoc/>
|
||||
public void Assert(Action assertion)
|
||||
{
|
||||
_isSurelyIdle = false;
|
||||
@@ -631,6 +602,8 @@ namespace Robust.UnitTesting
|
||||
await WaitIdleAsync();
|
||||
}
|
||||
|
||||
public virtual Task Cleanup() => Task.CompletedTask;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
@@ -654,7 +627,7 @@ namespace Robust.UnitTesting
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ServerIntegrationInstance : IntegrationInstance
|
||||
public sealed class ServerIntegrationInstance : IntegrationInstance, IServerIntegrationInstance
|
||||
{
|
||||
public ServerIntegrationInstance(ServerIntegrationOptions? options) : base(options)
|
||||
{
|
||||
@@ -724,7 +697,9 @@ namespace Robust.UnitTesting
|
||||
deps.BuildGraph();
|
||||
//ServerProgram.SetupLogging();
|
||||
ServerProgram.InitReflectionManager(deps);
|
||||
deps.Resolve<IReflectionManager>().LoadAssemblies(typeof(RobustIntegrationTest).Assembly);
|
||||
|
||||
if (Options?.LoadTestAssembly != false)
|
||||
deps.Resolve<IReflectionManager>().LoadAssemblies(typeof(RobustIntegrationTest).Assembly);
|
||||
|
||||
var server = DependencyCollection.Resolve<BaseServer>();
|
||||
|
||||
@@ -858,15 +833,17 @@ namespace Robust.UnitTesting
|
||||
}
|
||||
}
|
||||
|
||||
public override Task Cleanup() => RemoveAllDummySessions();
|
||||
|
||||
private Dictionary<string, NetUserId> _dummyUsers = new();
|
||||
private Dictionary<NetUserId, ICommonSession> _dummySessions = new();
|
||||
public IReadOnlyDictionary<string, NetUserId> DummyUsers => _dummyUsers;
|
||||
public IReadOnlyDictionary<NetUserId, ICommonSession> DummySessions => _dummySessions;
|
||||
}
|
||||
|
||||
public sealed class ClientIntegrationInstance : IntegrationInstance
|
||||
public sealed class ClientIntegrationInstance : IntegrationInstance, IClientIntegrationInstance
|
||||
{
|
||||
public ICommonSession? Session => ((IPlayerManager) PlayerMan).LocalSession;
|
||||
public ICommonSession? Session => PlayerMan.LocalSession;
|
||||
public NetUserId? User => Session?.UserId;
|
||||
public EntityUid? AttachedEntity => Session?.AttachedEntity;
|
||||
|
||||
@@ -903,10 +880,10 @@ namespace Robust.UnitTesting
|
||||
/// <summary>
|
||||
/// Wire up the server to connect to when <see cref="IClientNetManager.ClientConnect"/> gets called.
|
||||
/// </summary>
|
||||
public void SetConnectTarget(ServerIntegrationInstance server)
|
||||
public void SetConnectTarget(IServerIntegrationInstance server)
|
||||
{
|
||||
var clientNetManager = ResolveDependency<IntegrationNetManager>();
|
||||
var serverNetManager = server.ResolveDependency<IntegrationNetManager>();
|
||||
var serverNetManager = server.Resolve<IntegrationNetManager>();
|
||||
|
||||
if (!serverNetManager.IsRunning)
|
||||
{
|
||||
@@ -916,6 +893,14 @@ namespace Robust.UnitTesting
|
||||
clientNetManager.NextConnectChannel = serverNetManager.MessageChannelWriter;
|
||||
}
|
||||
|
||||
public async Task Connect(IServerIntegrationInstance target)
|
||||
{
|
||||
await WaitIdleAsync();
|
||||
await target.WaitIdleAsync();
|
||||
SetConnectTarget(target);
|
||||
await WaitPost(() => ((IClientNetManager) NetMan).ClientConnect(null!, 0, null!));
|
||||
}
|
||||
|
||||
public async Task CheckSandboxed(Assembly assembly)
|
||||
{
|
||||
await WaitIdleAsync();
|
||||
@@ -970,7 +955,9 @@ namespace Robust.UnitTesting
|
||||
deps.BuildGraph();
|
||||
|
||||
GameController.RegisterReflection(deps);
|
||||
deps.Resolve<IReflectionManager>().LoadAssemblies(typeof(RobustIntegrationTest).Assembly);
|
||||
|
||||
if (Options?.LoadTestAssembly != false)
|
||||
deps.Resolve<IReflectionManager>().LoadAssemblies(typeof(RobustIntegrationTest).Assembly);
|
||||
|
||||
var client = DependencyCollection.Resolve<GameController>();
|
||||
|
||||
@@ -1202,6 +1189,8 @@ namespace Robust.UnitTesting
|
||||
public Action? BeforeStart { get; set; }
|
||||
public Assembly[]? ContentAssemblies { get; set; }
|
||||
|
||||
public bool LoadTestAssembly { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// String containing extra prototypes to load. Contents of the string are treated like a yaml file in the
|
||||
/// resources folder.
|
||||
|
||||
Reference in New Issue
Block a user