using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Robust.Client;
using Robust.Server;
using Robust.Server.Console;
using Robust.Server.ServerStatus;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using ServerProgram = Robust.Server.Program;
namespace Robust.UnitTesting
{
///
/// Base class allowing you to implement integration tests.
///
///
/// Integration tests allow you to act upon a running server as a whole,
/// contrary to unit testing which tests, well, units.
///
public abstract partial class RobustIntegrationTest
{
internal static readonly ConcurrentQueue ClientsReady = new();
internal static readonly ConcurrentQueue ServersReady = new();
internal static readonly ConcurrentQueue ClientsCreated = new();
internal static readonly ConcurrentQueue ClientsPooled = new();
internal static readonly ConcurrentQueue ClientsNotPooled = new();
internal static readonly ConcurrentQueue ServersCreated = new();
internal static readonly ConcurrentQueue ServersPooled = new();
internal static readonly ConcurrentQueue ServersNotPooled = new();
private readonly List _notPooledInstances = new();
private readonly ConcurrentDictionary _clientsRunning = new();
private readonly ConcurrentDictionary _serversRunning = new();
private string TestId => TestContext.CurrentContext.Test.FullName;
private string GetTestsRanString(IntegrationInstance instance, string running)
{
var type = instance is ServerIntegrationInstance ? "Server " : "Client ";
return $"{type} tests ran ({instance.TestsRan.Count}):\n" +
$"{string.Join('\n', instance.TestsRan)}\n" +
$"Currently running: {running}";
}
///
/// Start an instance of the server and return an object that can be used to control it.
///
protected virtual ServerIntegrationInstance StartServer(ServerIntegrationOptions? options = null)
{
ServerIntegrationInstance instance;
if (ShouldPool(options))
{
if (ServersReady.TryDequeue(out var server))
{
server.PreviousOptions = server.ServerOptions;
server.ServerOptions = options;
OnServerReturn(server).Wait();
_serversRunning[server] = 0;
instance = server;
}
else
{
instance = new ServerIntegrationInstance(options);
_serversRunning[instance] = 0;
ServersCreated.Enqueue(TestId);
}
ServersPooled.Enqueue(TestId);
}
else
{
instance = new ServerIntegrationInstance(options);
_notPooledInstances.Add(instance);
ServersCreated.Enqueue(TestId);
ServersNotPooled.Enqueue(TestId);
}
var currentTest = TestContext.CurrentContext.Test.FullName;
TestContext.Out.WriteLine(GetTestsRanString(instance, currentTest));
instance.TestsRan.Add(currentTest);
return instance;
}
///
/// Start a headless instance of the client and return an object that can be used to control it.
///
protected virtual ClientIntegrationInstance StartClient(ClientIntegrationOptions? options = null)
{
ClientIntegrationInstance instance;
if (ShouldPool(options))
{
if (ClientsReady.TryDequeue(out var client))
{
client.PreviousOptions = client.ClientOptions;
client.ClientOptions = options;
OnClientReturn(client).Wait();
_clientsRunning[client] = 0;
instance = client;
}
else
{
instance = new ClientIntegrationInstance(options);
_clientsRunning[instance] = 0;
ClientsCreated.Enqueue(TestId);
}
ClientsPooled.Enqueue(TestId);
}
else
{
instance = new ClientIntegrationInstance(options);
_notPooledInstances.Add(instance);
ClientsCreated.Enqueue(TestId);
ClientsNotPooled.Enqueue(TestId);
}
var currentTest = TestContext.CurrentContext.Test.FullName;
TestContext.Out.WriteLine(GetTestsRanString(instance, currentTest));
instance.TestsRan.Add(currentTest);
return instance;
}
private bool ShouldPool(IntegrationOptions? options)
{
return options?.Pool ?? false;
}
protected virtual async Task OnInstanceReturn(IntegrationInstance instance)
{
await instance.WaitPost(() =>
{
var config = IoCManager.Resolve();
var overrides = new[]
{
(RTCVars.FailureLogLevel.Name, (instance.Options?.FailureLogLevel ?? RTCVars.FailureLogLevel.DefaultValue).ToString())
};
config.OverrideConVars(overrides);
});
}
protected virtual Task OnClientReturn(ClientIntegrationInstance client)
{
return OnInstanceReturn(client);
}
protected virtual Task OnServerReturn(ServerIntegrationInstance server)
{
return OnInstanceReturn(server);
}
[OneTimeTearDown]
public async Task OneTimeTearDown()
{
foreach (var client in _clientsRunning.Keys)
{
await client.WaitIdleAsync();
if (client.UnhandledException != null || !client.IsAlive)
{
continue;
}
ClientsReady.Enqueue(client);
}
_clientsRunning.Clear();
foreach (var server in _serversRunning.Keys)
{
await server.WaitIdleAsync();
if (server.UnhandledException != null || !server.IsAlive)
{
continue;
}
ServersReady.Enqueue(server);
}
_serversRunning.Clear();
_notPooledInstances.ForEach(p => p.Stop());
await Task.WhenAll(_notPooledInstances.Select(p => p.WaitIdleAsync()));
_notPooledInstances.Clear();
}
///
/// Provides control over a running instance of the client or server.
///
///
/// The instance executes in another thread.
/// As such, sending commands to it purely queues them to be ran asynchronously.
/// To ensure that the instance is idle, i.e. not executing code and finished all queued commands,
/// you can use .
/// This method must be used before trying to access any state like ,
/// to prevent race conditions.
///
public abstract class IntegrationInstance : IDisposable
{
private protected Thread? InstanceThread;
private protected IDependencyCollection DependencyCollection = default!;
private protected IntegrationGameLoop GameLoop = default!;
private protected readonly ChannelReader