Add dummy sessions for integration tests (#5202)

* Add dummy sessions

* if FULL_RELEASE
This commit is contained in:
Leon Friedrich
2024-06-05 18:50:06 +12:00
committed by GitHub
parent 3e3cd0e257
commit 75626a86a3
12 changed files with 286 additions and 32 deletions

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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())

View File

@@ -90,18 +90,18 @@ namespace Robust.Server.Player
_cfg.SyncConnectingClient(args.Channel);
}
private void EndSession(object? sender, NetChannelArgs args)
{
EndSession(args.Channel.UserId);
}
/// <summary>
/// Ends a clients session, and disconnects them.
/// </summary>
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;
}
}
}

View File

@@ -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;

View File

@@ -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; } = "<Unknown>";
public string Name { get; set; } = "<Unknown>";
[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<EntityUid> 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;
}
}

View File

@@ -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;
/// <summary>
/// This is a mock session for use with integration tests and benchmarks. It uses a <see cref="DummyChannel"/> as
/// its <see cref="INetChannel"/>, which doesn't support actually sending any messages.
/// </summary>
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<EntityUid> 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<byte>.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();
}
}
/// <summary>
/// A mock NetChannel for use in integration tests and benchmarks.
/// </summary>
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<T>() 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();
}
}

View File

@@ -43,9 +43,8 @@ public interface ICommonSession
/// <remarks>
/// 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.
/// </remarks>
INetChannel Channel { get; set; }
INetChannel Channel { get; [Obsolete] set; }
LoginType AuthType { get; }
@@ -75,3 +74,12 @@ public interface ICommonSession
/// </summary>
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);
}

View File

@@ -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);
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;

View File

@@ -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;

View File

@@ -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;
}
/// <summary>
/// Adds multiple dummy players to the server.
/// </summary>
public async Task<ICommonSession[]> AddDummySessions(int count)
{
var sessions = new ICommonSession[count];
for (var i = 0; i < sessions.Length; i++)
{
sessions[i] = await AddDummySession();
}
return sessions;
}
/// <summary>
/// Adds a dummy player to the server.
/// </summary>
public async Task<ICommonSession> 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;
}
/// <summary>
/// Removes a dummy player from the server.
/// </summary>
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);
}
/// <summary>
/// Removes all dummy players from the server.
/// </summary>
public async Task RemoveAllDummySessions()
{
foreach (var session in _dummySessions.Values)
{
await RemoveDummySession(session);
}
}
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