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