using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; using NUnit.Framework; 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; /// /// This object wraps a pooled server+client pair. /// public abstract partial class TestPair : 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!; protected TextWriter? Gravestone = default!; public Stopwatch Watch { get; } = new(); private List _testHistory = new(); public IReadOnlyList ExtendedTestHistory => _testHistory; 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> _loadedPrototypes = new(); private HashSet _loadedEntityPrototypes = new(); protected readonly Dictionary ModifiedClientCvars = new(); protected readonly Dictionary ModifiedServerCvars = new(); public async Task LoadPrototypes(List prototypes) { await LoadPrototypes(Server, prototypes); await LoadPrototypes(Client, prototypes); } public async Task Init( int id, BasePoolManager manager, PairSettings settings, TextWriter testOut, TextWriter? gravestone) { if (Initialized) throw new InvalidOperationException("Already initialized"); Id = id; Manager = manager; Settings = settings; Initialized = true; Gravestone = gravestone; if (Gravestone is not null) await Gravestone.WriteLineAsync("Test pair initialized."); ClientLogHandler.ActivateContext(testOut); ServerLogHandler.ActivateContext(testOut); // Need a new task so it doesn't wait until the first await (after the constructor) to run asynchronously var tasks = await Task.WhenAll(new[] {Task.Run(GenerateClientCast), Task.Run(GenerateServerCast)}); Client = (TClient) tasks[0]; Server = (TServer) tasks[1]; ActivateContext(testOut); await ApplySettings(settings); Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged; Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged; if (!settings.NoLoadTestPrototypes) await LoadPrototypes(Manager.TestPrototypes); var cRand = Client.Resolve(); var sRand = Server.Resolve(); _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 GenerateClient(); protected abstract Task GenerateServer(); protected async Task GenerateClientCast() { return await GenerateClient(); } protected async Task GenerateServerCast() { return await GenerateServer(); } public void Kill() { State = PairState.Dead; ServerLogHandler.ShuttingDown = true; ClientLogHandler.ShuttingDown = true; Server.Dispose(); Client.Dispose(); Gravestone?.WriteLine("Test pair killed."); Gravestone?.WriteLine(Environment.StackTrace); Gravestone?.Flush(); } private void ClearContext() { TestOut = default!; ServerLogHandler.ClearContext(); ClientLogHandler.ClearContext(); } public void ActivateContext(TextWriter testOut) { // This is the very, very first thing that happens in prepping a pair, so wait a sec // for disposal to get to finish if we got returned right this instant. // Not necessary after some of the disposal changes but defensive is good. 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 async Task AddToHistory(string testName) { var memUse = GC.GetTotalMemory(false); var entry = new TestHistoryEntry(testName, memUse); _testHistory.Add(entry); if (Gravestone is not null) { await Gravestone.WriteLineAsync($"#{_testHistory.Count}: {entry}"); await Gravestone.FlushAsync(); } } public void SetupSeed() { var sRand = Server.Resolve(); if (Settings.ServerSeed is { } severSeed) { ServerSeed = severSeed; sRand.SetSeed(ServerSeed); } else { ServerSeed = _nextServerSeed; sRand.SetSeed(ServerSeed); _nextServerSeed = sRand.Next(); } var cRand = Client.Resolve(); 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 prototypes) { var changed = new Dictionary>(); foreach (var file in prototypes) { instance.ProtoMan.LoadString(file, changed: changed); } await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed)); foreach (var (kind, ids) in changed) { _loadedPrototypes.GetOrNew(kind).UnionWith(ids); } if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds)) _loadedEntityPrototypes.UnionWith(entIds); } public 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(); } /// /// Reverts any cvars that were modified during a test back to their original values. /// 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(); } }