From 75626a86a33fe7a056c349fd593419d23ced643a Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:50:06 +1200 Subject: [PATCH] Add dummy sessions for integration tests (#5202) * Add dummy sessions * if FULL_RELEASE --- RELEASE-NOTES.md | 6 +- Robust.Client/Player/PlayerManager.cs | 10 +- Robust.Server/GameStates/PvsSystem.Session.cs | 3 +- Robust.Server/Player/PlayerManager.cs | 41 +++++- Robust.Shared/Network/NetManager.cs | 1 + Robust.Shared/Player/CommonSession.cs | 34 ++++- Robust.Shared/Player/DummySession.cs | 127 ++++++++++++++++++ Robust.Shared/Player/ICommonSession.cs | 12 +- .../Player/SharedPlayerManager.Sessions.cs | 14 +- .../Player/SharedPlayerManager.State.cs | 1 - .../RobustIntegrationTest.NetManager.cs | 5 + Robust.UnitTesting/RobustIntegrationTest.cs | 64 +++++++++ 12 files changed, 286 insertions(+), 32 deletions(-) create mode 100644 Robust.Shared/Player/DummySession.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 36642dcf9..d0fe621dc 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* `ServerIntegrationInstance` has new methods for adding dummy player sessions for tests that require multiple players. ### Bugfixes @@ -51,8 +51,8 @@ END TEMPLATE--> ### Internal -*None yet* - +* Added `DummySession` and `DummyChannel` classes for use in integration tests and benchmarks to fool the server into thinking that there are multiple players connected. +* Added `ICommonSessionInternal` and updated `CommonSession` so that the internal setters now go through that interface. ## 224.0.1 diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index 211748508..34b36f342 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -261,8 +261,8 @@ namespace Robust.Client.Player { // This is a new userid, so we create a new session. DebugTools.Assert(state.UserId != LocalPlayer?.UserId); - var newSession = (CommonSession) CreateAndAddSession(state.UserId, state.Name); - newSession.Ping = state.Ping; + var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name); + newSession.SetPing(state.Ping); SetStatus(newSession, state.Status); SetAttachedEntity(newSession, controlled, out _, true); dirty = true; @@ -279,9 +279,9 @@ namespace Robust.Client.Player } dirty = true; - var local = (CommonSession) session; - local.Name = state.Name; - local.Ping = state.Ping; + var local = (ICommonSessionInternal)session; + local.SetName(state.Name); + local.SetPing(state.Ping); SetStatus(local, state.Status); SetAttachedEntity(local, controlled, out _, true); } diff --git a/Robust.Server/GameStates/PvsSystem.Session.cs b/Robust.Server/GameStates/PvsSystem.Session.cs index 98fe1dd41..264cd7341 100644 --- a/Robust.Server/GameStates/PvsSystem.Session.cs +++ b/Robust.Server/GameStates/PvsSystem.Session.cs @@ -7,6 +7,7 @@ using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -42,7 +43,7 @@ internal sealed partial class PvsSystem // PVS benchmarks use dummy sessions. // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (session.Channel != null) + if (session.Channel is not DummyChannel) { _netMan.ServerSendMessage(msg, session.Channel); if (msg.ShouldSendReliably()) diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index 61f562691..f1673941b 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -90,18 +90,18 @@ namespace Robust.Server.Player _cfg.SyncConnectingClient(args.Channel); } + private void EndSession(object? sender, NetChannelArgs args) + { + EndSession(args.Channel.UserId); + } + /// /// Ends a clients session, and disconnects them. /// - private void EndSession(object? sender, NetChannelArgs args) + internal void EndSession(NetUserId user) { - if (!TryGetSessionByChannel(args.Channel, out var session)) - { + if (!TryGetSessionById(user, out var session)) return; - } - - // make sure nothing got messed up during the life of the session - DebugTools.Assert(session.Channel == args.Channel); SetStatus(session, SessionStatus.Disconnected); SetAttachedEntity(session, null, out _, true); @@ -159,5 +159,32 @@ namespace Robust.Server.Player session = actor.PlayerSession; return true; } + + internal ICommonSession AddDummySession(NetUserId user, string name) + { +#if FULL_RELEASE + // Lets not make it completely trivial to fake player counts. + throw new NotSupportedException(); +#endif + Lock.EnterWriteLock(); + DummySession session; + try + { + UserIdMap[name] = user; + if (!PlayerData.TryGetValue(user, out var data)) + PlayerData[user] = data = new(user, name); + + session = new DummySession(user, name, data); + InternalSessions.Add(user, session); + } + finally + { + Lock.ExitWriteLock(); + } + + UpdateState(session); + + return session; + } } } diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 1a814eed3..74cb48bbc 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -12,6 +12,7 @@ using Prometheus; using Robust.Shared.Configuration; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Player; using Robust.Shared.Profiling; using Robust.Shared.Serialization; using Robust.Shared.Timing; diff --git a/Robust.Shared/Player/CommonSession.cs b/Robust.Shared/Player/CommonSession.cs index 3dbc40459..bf520b4dd 100644 --- a/Robust.Shared/Player/CommonSession.cs +++ b/Robust.Shared/Player/CommonSession.cs @@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables; namespace Robust.Shared.Player; -internal sealed class CommonSession : ICommonSession +internal sealed class CommonSession : ICommonSessionInternal { [ViewVariables] public EntityUid? AttachedEntity { get; set; } @@ -17,10 +17,10 @@ internal sealed class CommonSession : ICommonSession public NetUserId UserId { get; } [ViewVariables] - public string Name { get; internal set; } = ""; + public string Name { get; set; } = ""; [ViewVariables] - public short Ping { get; internal set; } + public short Ping { get; set; } [ViewVariables] public DateTime ConnectedTime { get; set; } @@ -42,9 +42,6 @@ internal sealed class CommonSession : ICommonSession [ViewVariables] public HashSet ViewSubscriptions { get; } = new(); - [ViewVariables] - public int VisibilityMask { get; set; } = 1; - [ViewVariables] public LoginType AuthType => Channel?.AuthType ?? default; @@ -56,4 +53,29 @@ internal sealed class CommonSession : ICommonSession Name = name; Data = data; } + + public void SetStatus(SessionStatus status) + { + Status = status; + } + + public void SetAttachedEntity(EntityUid? uid) + { + AttachedEntity = uid; + } + + public void SetPing(short ping) + { + Ping = ping; + } + + public void SetName(string name) + { + Name = name; + } + + public void SetChannel(INetChannel channel) + { + Channel = channel; + } } diff --git a/Robust.Shared/Player/DummySession.cs b/Robust.Shared/Player/DummySession.cs new file mode 100644 index 000000000..f56adaf05 --- /dev/null +++ b/Robust.Shared/Player/DummySession.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Net; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Network; + +namespace Robust.Shared.Player; + +/// +/// This is a mock session for use with integration tests and benchmarks. It uses a as +/// its , which doesn't support actually sending any messages. +/// +internal sealed class DummySession : ICommonSessionInternal +{ + public EntityUid? AttachedEntity {get; set; } + public SessionStatus Status { get; set; } = SessionStatus.Connecting; + public NetUserId UserId => UserData.UserId; + public string Name => UserData.UserName; + + public short Ping { get; set; } + + public INetChannel Channel + { + get => DummyChannel; + [Obsolete] + set => throw new NotSupportedException(); + } + + public LoginType AuthType { get; set; } = LoginType.GuestAssigned; + public HashSet ViewSubscriptions { get; } = new(); + public DateTime ConnectedTime { get; set; } + public SessionState State { get; set; } = new(); + public SessionData Data { get; set; } + public bool ClientSide { get; set; } + public NetUserData UserData { get; set; } + + public DummyChannel DummyChannel; + + public DummySession(NetUserId userId, string userName, SessionData data) + { + Data = data; + UserData = new(userId, userName) + { + HWId = ImmutableArray.Empty + }; + DummyChannel = new(this); + } + + public void SetStatus(SessionStatus status) + { + Status = status; + } + + public void SetAttachedEntity(EntityUid? uid) + { + AttachedEntity = uid; + } + + public void SetPing(short ping) + { + Ping = ping; + } + + public void SetName(string name) + { + UserData = new(UserData.UserId, name) + { + HWId = UserData.HWId + }; + } + + public void SetChannel(INetChannel channel) + { + throw new NotSupportedException(); + } +} + +/// +/// A mock NetChannel for use in integration tests and benchmarks. +/// +internal sealed class DummyChannel(DummySession session) : INetChannel +{ + public readonly DummySession Session = session; + public NetUserData UserData => Session.UserData; + public short Ping => Session.Ping; + public string UserName => Session.Name; + public LoginType AuthType => Session.AuthType; + public NetUserId UserId => Session.UserId; + + public int CurrentMtu { get; set; } = default; + public long ConnectionId { get; set; } = default; + public TimeSpan RemoteTimeOffset { get; set; } = default; + public TimeSpan RemoteTime { get; set; } = default; + public bool IsConnected { get; set; } = true; + public bool IsHandshakeComplete { get; set; } = true; + + // This is just pilfered from IntegrationNetChannel + public IPEndPoint RemoteEndPoint { get; } = new(IPAddress.Loopback, 1212); + + // Only used on server, contains the encryption to use for this channel. + public NetEncryption? Encryption { get; set; } + + public INetManager NetPeer => throw new NotImplementedException(); + + public T CreateNetMessage() where T : NetMessage, new() + { + throw new NotImplementedException(); + } + + public void SendMessage(NetMessage message) + { + throw new NotImplementedException(); + } + + public void Disconnect(string reason) + { + throw new NotImplementedException(); + } + + public void Disconnect(string reason, bool sendBye) + { + throw new NotImplementedException(); + } +} diff --git a/Robust.Shared/Player/ICommonSession.cs b/Robust.Shared/Player/ICommonSession.cs index 3ff1ff00e..b5ef46aa9 100644 --- a/Robust.Shared/Player/ICommonSession.cs +++ b/Robust.Shared/Player/ICommonSession.cs @@ -43,9 +43,8 @@ public interface ICommonSession /// /// On the Server every player has a network channel, /// on the Client only the LocalPlayer has a network channel, and that channel points to the server. - /// Unless you know what you are doing, you shouldn't be modifying this directly. /// - INetChannel Channel { get; set; } + INetChannel Channel { get; [Obsolete] set; } LoginType AuthType { get; } @@ -75,3 +74,12 @@ public interface ICommonSession /// bool ClientSide { get; set; } } + +internal interface ICommonSessionInternal : ICommonSession +{ + public void SetStatus(SessionStatus status); + public void SetAttachedEntity(EntityUid? uid); + public void SetPing(short ping); + public void SetName(string name); + void SetChannel(INetChannel channel); +} diff --git a/Robust.Shared/Player/SharedPlayerManager.Sessions.cs b/Robust.Shared/Player/SharedPlayerManager.Sessions.cs index d0d958e57..0c60bad59 100644 --- a/Robust.Shared/Player/SharedPlayerManager.Sessions.cs +++ b/Robust.Shared/Player/SharedPlayerManager.Sessions.cs @@ -101,8 +101,8 @@ internal abstract partial class SharedPlayerManager public ICommonSession CreateAndAddSession(INetChannel channel) { - var session = CreateAndAddSession(channel.UserId, channel.UserName); - session.Channel = channel; + var session = (ICommonSessionInternal)CreateAndAddSession(channel.UserId, channel.UserName); + session.SetChannel(channel); return session; } @@ -176,7 +176,7 @@ internal abstract partial class SharedPlayerManager if (session.AttachedEntity is not {} uid) return; - ((CommonSession) session).AttachedEntity = null; + ((ICommonSessionInternal) session).SetAttachedEntity(null); UpdateState(session); if (EntManager.TryGetComponent(uid, out ActorComponent? actor) && actor.LifeStage <= ComponentLifeStage.Running) @@ -215,7 +215,7 @@ internal abstract partial class SharedPlayerManager if (session.AttachedEntity != null) Detach(session); - ((CommonSession) session).AttachedEntity = uid; + ((ICommonSessionInternal) session).SetAttachedEntity(uid); actor.PlayerSession = session; UpdateState(session); EntManager.EventBus.RaiseLocalEvent(uid, new PlayerAttachedEvent(uid, session), true); @@ -228,7 +228,7 @@ internal abstract partial class SharedPlayerManager return; var old = session.Status; - ((CommonSession) session).Status = status; + ((ICommonSessionInternal) session).SetStatus(status); UpdateState(session); PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(session, old, status)); @@ -236,13 +236,13 @@ internal abstract partial class SharedPlayerManager public void SetPing(ICommonSession session, short ping) { - ((CommonSession) session).Ping = ping; + ((ICommonSessionInternal) session).SetPing(ping); UpdateState(session); } public void SetName(ICommonSession session, string name) { - ((CommonSession) session).Name = name; + ((ICommonSessionInternal) session).SetName(name); UpdateState(session); } diff --git a/Robust.Shared/Player/SharedPlayerManager.State.cs b/Robust.Shared/Player/SharedPlayerManager.State.cs index d17a90980..199561625 100644 --- a/Robust.Shared/Player/SharedPlayerManager.State.cs +++ b/Robust.Shared/Player/SharedPlayerManager.State.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Robust.Shared.GameStates; using Robust.Shared.Timing; diff --git a/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs b/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs index 29a5d333d..7553aa27a 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs @@ -11,6 +11,7 @@ using Robust.Shared.Asynchronous; using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Network.Messages; +using Robust.Shared.Player; using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -258,6 +259,9 @@ namespace Robust.UnitTesting { DebugTools.Assert(IsServer); + if (recipient is DummyChannel) + return; + var channel = (IntegrationNetChannel) recipient; channel.OtherChannel.TryWrite(SerializeNetMessage(message, channel.RemoteUid)); } @@ -443,6 +447,7 @@ namespace Robust.UnitTesting public int CurrentMtu => 1000; // Arbitrary. // TODO: Should this port value make sense? + // See also the DummyChannel class public IPEndPoint RemoteEndPoint { get; } = new(IPAddress.Loopback, 1212); public NetUserId UserId => UserData.UserId; public string UserName => UserData.UserName; diff --git a/Robust.UnitTesting/RobustIntegrationTest.cs b/Robust.UnitTesting/RobustIntegrationTest.cs index a26a545e9..16231c687 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.cs @@ -26,6 +26,7 @@ using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.ContentPack; +using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; @@ -761,6 +762,69 @@ namespace Robust.UnitTesting pvs.SendGameStates(players); Timing.CurTick += 1; } + + /// + /// Adds multiple dummy players to the server. + /// + public async Task AddDummySessions(int count) + { + var sessions = new ICommonSession[count]; + for (var i = 0; i < sessions.Length; i++) + { + sessions[i] = await AddDummySession(); + } + + return sessions; + } + + /// + /// Adds a dummy player to the server. + /// + public async Task AddDummySession(string? userName = null) + { + userName ??= $"integration_dummy_{DummyUsers.Count}"; + Log.Info($"Adding dummy session {userName}"); + if (!_dummyUsers.TryGetValue(userName, out var userId)) + _dummyUsers[userName] = userId = new(Guid.NewGuid()); + + var man = (Robust.Server.Player.PlayerManager) PlayerMan; + var session = man.AddDummySession(userId, userName); + _dummySessions.Add(userId, session); + + session.ConnectedTime = DateTime.UtcNow; + await WaitPost(() => man.SetStatus(session, SessionStatus.Connected)); + + return session; + } + + /// + /// Removes a dummy player from the server. + /// + public async Task RemoveDummySession(ICommonSession session, bool removeUser = false) + { + Log.Info($"Removing dummy session {session.Name}"); + _dummySessions.Remove(session.UserId); + var man = (Robust.Server.Player.PlayerManager) PlayerMan; + await WaitPost(() => man.EndSession(session.UserId)); + if (removeUser) + _dummyUsers.Remove(session.Name); + } + + /// + /// Removes all dummy players from the server. + /// + public async Task RemoveAllDummySessions() + { + foreach (var session in _dummySessions.Values) + { + await RemoveDummySession(session); + } + } + + private Dictionary _dummyUsers = new(); + private Dictionary _dummySessions = new(); + public IReadOnlyDictionary DummyUsers => _dummyUsers; + public IReadOnlyDictionary DummySessions => _dummySessions; } public sealed class ClientIntegrationInstance : IntegrationInstance