first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
using Nitrox.Test.Client.Communication;
using NitroxModel.Packets;
namespace NitroxClient.Communication;
[TestClass]
public class DeferredPacketReceiverTest
{
[TestMethod]
public void NonActionPacket()
{
// Arrange
const ushort PLAYER_ID = 1;
TestNonActionPacket packet = new(PLAYER_ID);
PacketReceiver packetReceiver = new();
// Act
packetReceiver.Add(packet);
Packet storedPacket = packetReceiver.GetNextPacket();
// Assert
storedPacket.Should().NotBeNull();
packetReceiver.GetNextPacket().Should().BeNull();
storedPacket.Should().Be(packet);
packet.PlayerId.Should().Be(PLAYER_ID);
}
}

View File

@@ -0,0 +1,91 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxClient.Communication.Abstract;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
[TestClass]
public class SessionJoinedStateTests
{
[TestMethod]
public void NegotiateShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
SessionJoined connectionState = new SessionJoined();
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void JoinSessionShouldThrowInvalidOperationExcepion()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
SessionJoined connectionState = new SessionJoined();
// Act
Action action = () => connectionState.JoinSession(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void DisconnectShouldStopTheClient()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionJoined connectionState = new SessionJoined();
// Act
connectionState.Disconnect(connectionContext);
// Assert
serverClient.Received().Stop();
}
[TestMethod]
public void DisconnectShouldResetTheConnectionContext()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionJoined connectionState = new SessionJoined();
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().ClearSessionState();
}
[TestMethod]
public void DisconnectShouldTransitionToDisconnectedState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionJoined connection = new SessionJoined();
// Act
connection.Disconnect(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<Disconnected>());
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxClient.Communication.Abstract;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
[TestClass]
public class SessionReservationRejectedStateTests
{
[TestMethod]
public void NegotiateShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
SessionReservationRejected connectionState = new SessionReservationRejected();
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void JoinSessionShouldThrowInvalidOperationExcepion()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
SessionReservationRejected connectionState = new SessionReservationRejected();
// Act
Action action = () => connectionState.JoinSession(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void DisconnectShouldStopTheClient()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionReservationRejected connectionState = new SessionReservationRejected();
// Act
connectionState.Disconnect(connectionContext);
// Assert
serverClient.Received().Stop();
}
[TestMethod]
public void DisconnectShouldResetTheConnectionContext()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionReservationRejected connectionState = new SessionReservationRejected();
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().ClearSessionState();
}
[TestMethod]
public void DisconnectShouldTransitionToDisconnectedState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionReservationRejected connection = new SessionReservationRejected();
// Act
connection.Disconnect(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<Disconnected>());
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Client.Communication.MultiplayerSession;
using NitroxClient.Communication.Abstract;
using NitroxModel.Packets;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
[TestClass]
public class SessionReservedStateTests
{
[TestMethod]
public void NegotiateShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
SessionReserved connectionState = new SessionReserved();
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void JoinSessionShouldSendPlayerJoiningMultiplayerSessionPacket()
{
// Arrange
MultiplayerSessionReservation successfulReservation = new MultiplayerSessionReservation(
TestConstants.TEST_CORRELATION_ID,
TestConstants.TEST_PLAYER_ID,
TestConstants.TEST_RESERVATION_KEY);
IClient client = Substitute.For<IClient>();
client.IsConnected.Returns(true);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Reservation.Returns(successfulReservation);
connectionContext.Client.Returns(client);
SessionReserved connectionState = new SessionReserved();
// Act
connectionState.JoinSession(connectionContext);
// Assert
client.Received().Send(Arg.Any<PlayerJoiningMultiplayerSession>());
}
[TestMethod]
public void JoinSessionShouldTransitionToSessionJoinedState()
{
// Arrange
MultiplayerSessionReservation successfulReservation = new MultiplayerSessionReservation(
TestConstants.TEST_CORRELATION_ID,
TestConstants.TEST_PLAYER_ID,
TestConstants.TEST_RESERVATION_KEY);
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(true);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Reservation.Returns(successfulReservation);
connectionContext.Client.Returns(serverClient);
SessionReserved connection = new SessionReserved();
// Act
connection.JoinSession(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<SessionJoined>());
}
[TestMethod]
public void DisconnectShouldStopTheClient()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionReserved connectionState = new SessionReserved();
// Act
connectionState.Disconnect(connectionContext);
// Assert
serverClient.Received().Stop();
}
[TestMethod]
public void DisconnectShouldResetTheConnectionContext()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionReserved connectionState = new SessionReserved();
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().ClearSessionState();
}
[TestMethod]
public void DisconnectShouldTransitionToDisconnectedState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
SessionReserved connection = new SessionReserved();
// Act
connection.Disconnect(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<Disconnected>());
}
}
}

View File

@@ -0,0 +1,162 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Client.Communication.MultiplayerSession;
using NitroxClient.Communication.Abstract;
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
[TestClass]
public class AwaitingReservationCredentialsStateTests
{
[TestMethod]
public void NegotiateShouldSendServerAuthorityReservationRequest()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(true);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
connectionContext.PlayerSettings.Returns(TestConstants.TEST_PLAYER_SETTINGS);
connectionContext.AuthenticationContext.Returns(TestConstants.TEST_AUTHENTICATION_CONTEXT);
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
serverClient.Received().Send(Arg.Any<MultiplayerSessionReservationRequest>());
}
[TestMethod]
public void NegotiateShouldTransitionToAwaitingSessionReservationState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(true);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
connectionContext.PlayerSettings.Returns(TestConstants.TEST_PLAYER_SETTINGS);
connectionContext.AuthenticationContext.Returns(TestConstants.TEST_AUTHENTICATION_CONTEXT);
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<AwaitingSessionReservation>());
}
[TestMethod]
public void NegotiateShouldThrowInvalidOperationExceptionWhenPlayerSettingsIsNull()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(true);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.PlayerSettings.Returns((PlayerSettings)null);
connectionContext.AuthenticationContext.Returns(TestConstants.TEST_AUTHENTICATION_CONTEXT);
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void NegotiateShouldThrowInvalidOperationExceptionWhenAuthenticationContextIsNull()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(true);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.SessionPolicy.Returns(TestConstants.TEST_SESSION_POLICY);
connectionContext.AuthenticationContext.Returns((AuthenticationContext)null);
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void JoinSessionShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
Action action = () => connectionState.JoinSession(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void DisconnectShouldStopTheClient()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
connectionState.Disconnect(connectionContext);
// Assert
serverClient.Received().Stop();
}
[TestMethod]
public void DisconnectShouldResetTheConnectionContext()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().ClearSessionState();
}
[TestMethod]
public void DisconnectShouldTransitionToDisconnectedState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
AwaitingReservationCredentials connectionState = new AwaitingReservationCredentials();
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<Disconnected>());
}
}
}

View File

@@ -0,0 +1,156 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Client.Communication.MultiplayerSession;
using NitroxClient.Communication.Abstract;
using NitroxModel.Packets;
using NitroxModel.Packets.Exceptions;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
[TestClass]
public class AwaitingSessionReservationStateTests
{
[TestMethod]
public void NegotiateShouldTransitionToSessionRevervedAfterReceivingSuccessfulReservation()
{
// Arrange
MultiplayerSessionReservation successfulReservation = new MultiplayerSessionReservation(
TestConstants.TEST_CORRELATION_ID,
TestConstants.TEST_PLAYER_ID,
TestConstants.TEST_RESERVATION_KEY);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Reservation.Returns(successfulReservation);
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<SessionReserved>());
}
[TestMethod]
public void NegotiateShouldThrowUncorrelatedPacketExceptionWhenTheReservationHasTheWrongCorrelationId()
{
// Arrange
MultiplayerSessionReservation successfulReservation = new MultiplayerSessionReservation(
"wrong",
TestConstants.TEST_PLAYER_ID,
TestConstants.TEST_RESERVATION_KEY);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Reservation.Returns(successfulReservation);
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<UncorrelatedPacketException>();
}
[TestMethod]
public void NegotiateShouldTransitionToSessionReservationRejectedAfterReceivingRejectedReservation()
{
// Arrange
MultiplayerSessionReservation rejectedReservation = new MultiplayerSessionReservation(TestConstants.TEST_CORRELATION_ID, TestConstants.TEST_REJECTION_STATE);
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Reservation.Returns(rejectedReservation);
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<SessionReservationRejected>());
}
[TestMethod]
public void NegotiateShouldThrowInvalidOperationExceptionWhenTheReservationIsNull()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Reservation.Returns((MultiplayerSessionReservation)null);
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void JoinSessionShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
Action action = () => connectionState.JoinSession(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void DisconnectShouldStopTheClient()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.Disconnect(connectionContext);
// Assert
serverClient.Received().Stop();
}
[TestMethod]
public void DisconnectShouldResetTheConnectionContext()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().ClearSessionState();
}
[TestMethod]
public void DisconnectShouldTransitionToDisconnectedState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
AwaitingSessionReservation connectionState = new AwaitingSessionReservation(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<Disconnected>());
}
}
}

View File

@@ -0,0 +1,144 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Client.Communication.MultiplayerSession;
using NitroxClient.Communication.Abstract;
using NitroxModel.Packets;
using NitroxModel.Packets.Exceptions;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
[TestClass]
public class EstablishingSessionPolicyStateTests
{
[TestMethod]
public void NegotiateShouldTransitionToAwaitingReservationCredentialsState()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.SessionPolicy.Returns(new MultiplayerSessionPolicy(TestConstants.TEST_CORRELATION_ID, false, TestConstants.TEST_MAX_PLAYER_CONNECTIONS, false));
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<AwaitingReservationCredentials>());
}
[TestMethod]
public void NegotiateShouldTransitionToAwaitingReservationCredentialsStateWithPassword()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.SessionPolicy.Returns(new MultiplayerSessionPolicy(TestConstants.TEST_CORRELATION_ID, false, TestConstants.TEST_MAX_PLAYER_CONNECTIONS, true));
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<AwaitingReservationCredentials>());
}
[TestMethod]
public void NegotiateShouldThrowUncorrelatedPacketExceptionWhenThePolicyHasTheWrongCorrelationId()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.SessionPolicy.Returns(new MultiplayerSessionPolicy("wrong", false, TestConstants.TEST_MAX_PLAYER_CONNECTIONS, false));
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<UncorrelatedPacketException>();
}
[TestMethod]
public void NegotiateShouldThrowInvalidOperationExceptionIfTheSessionPolicyIsNull()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.SessionPolicy.Returns((MultiplayerSessionPolicy)null);
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
Action action = () => connectionState.NegotiateReservationAsync(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void JoinSessionShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
Action action = () => connectionState.JoinSession(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void DisconnectShouldStopTheClient()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.Disconnect(connectionContext);
// Assert
serverClient.Received().Stop();
}
[TestMethod]
public void DisconnectShouldResetTheConnectionContext()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().ClearSessionState();
}
[TestMethod]
public void DisconnectShouldTransitionToDisconnectedState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
EstablishingSessionPolicy connectionState = new EstablishingSessionPolicy(TestConstants.TEST_CORRELATION_ID);
// Act
connectionState.Disconnect(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<Disconnected>());
}
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Client.Communication.MultiplayerSession;
using NitroxClient.Communication.Abstract;
using NitroxModel.Packets;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
[TestClass]
public class DisconnectedStateTests
{
[TestMethod]
public void NegotiateShouldStartTheClientOnTheContext()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(false);
serverClient
.When(client => client.StartAsync(Arg.Any<string>(), TestConstants.TEST_SERVER_PORT))
.Do(info => serverClient.IsConnected.Returns(true));
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
connectionContext.ServerPort.Returns(TestConstants.TEST_SERVER_PORT);
Disconnected connectionState = new Disconnected();
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
serverClient.IsConnected.Should().BeTrue();
}
[TestMethod]
public void NegotiateShouldSendMultiplayerSessionPolicyRequestPacketToClient()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(false);
serverClient
.When(client => client.StartAsync(Arg.Any<string>(), TestConstants.TEST_SERVER_PORT))
.Do(info => serverClient.IsConnected.Returns(true));
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
connectionContext.ServerPort.Returns(TestConstants.TEST_SERVER_PORT);
Disconnected connectionState = new Disconnected();
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
serverClient.Received().Send(Arg.Any<MultiplayerSessionPolicyRequest>());
}
[TestMethod]
public void NegotiateShouldTransitionToEstablishingSessionPolicyState()
{
// Arrange
IClient serverClient = Substitute.For<IClient>();
serverClient.IsConnected.Returns(false);
serverClient
.When(client => client.StartAsync(Arg.Any<string>(), TestConstants.TEST_SERVER_PORT))
.Do(info => serverClient.IsConnected.Returns(true));
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(serverClient);
connectionContext.ServerPort.Returns(TestConstants.TEST_SERVER_PORT);
Disconnected connectionState = new Disconnected();
// Act
connectionState.NegotiateReservationAsync(connectionContext);
// Assert
connectionContext.Received().UpdateConnectionState(Arg.Any<EstablishingSessionPolicy>());
}
[TestMethod]
public async Task NegotiateShouldThrowInvalidOperationExceptionWhenClientIsNull()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns((IClient)null);
connectionContext.IpAddress.Returns(TestConstants.TEST_IP_ADDRESS);
Disconnected connectionState = new Disconnected();
// Act
Func<Task> action = async () => await connectionState.NegotiateReservationAsync(connectionContext);
// Assert
await action.Should().ThrowAsync<InvalidOperationException>();
}
[TestMethod]
public async Task NegotiateShouldThrowInvalidOperationExceptionWhenIpAddressIsNull()
{
// Arrange
IClient client = Substitute.For<IClient>();
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
connectionContext.Client.Returns(client);
connectionContext.IpAddress.Returns((string)null);
Disconnected connectionState = new Disconnected();
// Act
Func<Task> action = async () => await connectionState.NegotiateReservationAsync(connectionContext);
// Assert
await action.Should().ThrowAsync<InvalidOperationException>();
}
[TestMethod]
public void JoinSessionShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
Disconnected connectionState = new Disconnected();
// Act
Action action = () => connectionState.JoinSession(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void DisconnectShouldThrowInvalidOperationException()
{
// Arrange
IMultiplayerSessionConnectionContext connectionContext = Substitute.For<IMultiplayerSessionConnectionContext>();
Disconnected connectionState = new Disconnected();
// Act
Action action = () => connectionState.Disconnect(connectionContext);
// Assert
action.Should().Throw<InvalidOperationException>();
}
}
}

View File

@@ -0,0 +1,119 @@
using FluentAssertions;
using FluentAssertions.Events;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Client.Communication.MultiplayerSession;
using NitroxClient.Communication.Abstract;
using NitroxModel.Packets;
using NSubstitute;
namespace NitroxClient.Communication.MultiplayerSession
{
[TestClass]
public class MultiplayerSessionMangerTests
{
[TestMethod]
public void ManagerShouldInitializeInDisconnectedStage()
{
// Arrange
IClient client = Substitute.For<IClient>();
// Act
IMultiplayerSession multiplayerSession = new MultiplayerSessionManager(client);
// Assert
multiplayerSession.CurrentState.CurrentStage.Should().Be(MultiplayerSessionConnectionStage.DISCONNECTED);
}
[TestMethod]
public void ManagerShouldInitializeWithClient()
{
// Arrange
IClient client = Substitute.For<IClient>();
// Act
IMultiplayerSession multiplayerSession = new MultiplayerSessionManager(client);
// Assert
multiplayerSession.Client.Should().Be(client);
}
[TestMethod]
public void ConnectShouldSetIpAddress()
{
// Arrange
IClient client = Substitute.For<IClient>();
IMultiplayerSession multiplayerSession = new MultiplayerSessionManager(client, TestConstants.TEST_CONNECTION_STATE);
// Act
multiplayerSession.ConnectAsync(TestConstants.TEST_IP_ADDRESS, TestConstants.TEST_SERVER_PORT);
// Assert
multiplayerSession.IpAddress.Should().Be(TestConstants.TEST_IP_ADDRESS);
multiplayerSession.ServerPort.Should().Be(TestConstants.TEST_SERVER_PORT);
}
[TestMethod]
public void ProcessSessionPolicyShouldSetThePolicy()
{
// Arrange
IClient client = Substitute.For<IClient>();
IMultiplayerSession multiplayerSession = new MultiplayerSessionManager(client, TestConstants.TEST_CONNECTION_STATE);
// Act
multiplayerSession.ProcessSessionPolicy(TestConstants.TEST_SESSION_POLICY);
// Assert
multiplayerSession.SessionPolicy.Should().Be(TestConstants.TEST_SESSION_POLICY);
}
[TestMethod]
public void RequestSessionReservationShouldSetSettingsAndAuthContext()
{
// Arrange
IClient client = Substitute.For<IClient>();
IMultiplayerSession multiplayerSession = new MultiplayerSessionManager(client, TestConstants.TEST_CONNECTION_STATE);
// Act
multiplayerSession.RequestSessionReservation(TestConstants.TEST_PLAYER_SETTINGS, TestConstants.TEST_AUTHENTICATION_CONTEXT);
// Assert
multiplayerSession.PlayerSettings.Should().Be(TestConstants.TEST_PLAYER_SETTINGS);
multiplayerSession.AuthenticationContext.Should().Be(TestConstants.TEST_AUTHENTICATION_CONTEXT);
}
[TestMethod]
public void ProcessReservationResponsePacketShouldSetTheReservation()
{
// Arrange
MultiplayerSessionReservation successfulReservation = new MultiplayerSessionReservation(
TestConstants.TEST_CORRELATION_ID,
TestConstants.TEST_PLAYER_ID,
TestConstants.TEST_RESERVATION_KEY);
IClient client = Substitute.For<IClient>();
IMultiplayerSession multiplayerSession = new MultiplayerSessionManager(client, TestConstants.TEST_CONNECTION_STATE);
// Act
multiplayerSession.ProcessReservationResponsePacket(successfulReservation);
// Assert
multiplayerSession.Reservation.Should().Be(successfulReservation);
}
[TestMethod]
public void UpdateStateShouldRaiseEvent()
{
// Arrange
IClient client = Substitute.For<IClient>();
IMultiplayerSession multiplayerSession = new MultiplayerSessionManager(client);
IMultiplayerSessionConnectionContext connectionContext = (IMultiplayerSessionConnectionContext)multiplayerSession;
IMonitor<IMultiplayerSession> monitor = multiplayerSession.Monitor();
// Act
connectionContext.UpdateConnectionState(TestConstants.TEST_CONNECTION_STATE);
// Assert
monitor.Should().Raise("ConnectionStateChanged");
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Communication.Abstract;
using NitroxModel.DataStructures.Util;
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets;
using NSubstitute;
namespace Nitrox.Test.Client.Communication.MultiplayerSession
{
internal static class TestConstants
{
public const string TEST_IP_ADDRESS = "#.#.#.#";
public const int TEST_SERVER_PORT = 11000;
public const ushort TEST_PLAYER_ID = 1;
public const string TEST_PLAYER_NAME = "TEST";
public const string TEST_RESERVATION_KEY = "@#*(&";
public const string TEST_CORRELATION_ID = "CORRELATED";
public const int TEST_MAX_PLAYER_CONNECTIONS = 100;
public const MultiplayerSessionReservationState TEST_REJECTION_STATE = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.UNIQUE_PLAYER_NAME_CONSTRAINT_VIOLATED;
public static readonly AuthenticationContext TEST_AUTHENTICATION_CONTEXT = new AuthenticationContext(TEST_PLAYER_NAME, Optional.Empty);
public static readonly MultiplayerSessionPolicy TEST_SESSION_POLICY = new MultiplayerSessionPolicy(TEST_CORRELATION_ID, false, TEST_MAX_PLAYER_CONNECTIONS, false);
public static readonly PlayerSettings TEST_PLAYER_SETTINGS = new PlayerSettings(RandomColorGenerator.GenerateColor());
public static readonly IMultiplayerSessionConnectionState TEST_CONNECTION_STATE = Substitute.For<IMultiplayerSessionConnectionState>();
}
}

View File

@@ -0,0 +1,53 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxModel.Packets;
namespace NitroxClient.Communication;
[TestClass]
public class PacketSuppressorTest
{
[TestMethod]
public void SingleSuppress()
{
Assert.IsFalse(PacketSuppressor<BedEnter>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODAssetPacket>.IsSuppressed);
using (PacketSuppressor<BedEnter>.Suppress())
{
Assert.IsTrue(PacketSuppressor<BedEnter>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODAssetPacket>.IsSuppressed);
}
Assert.IsFalse(PacketSuppressor<BedEnter>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODAssetPacket>.IsSuppressed);
}
[TestMethod]
public void MultipleSuppress()
{
Assert.IsFalse(PacketSuppressor<BedEnter>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODAssetPacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODEventInstancePacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODCustomEmitterPacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODCustomLoopingEmitterPacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODStudioEmitterPacket>.IsSuppressed);
using (PacketSuppressor<FMODAssetPacket, FMODEventInstancePacket, FMODCustomEmitterPacket, FMODCustomLoopingEmitterPacket, FMODStudioEmitterPacket>.Suppress())
{
Assert.IsFalse(PacketSuppressor<BedEnter>.IsSuppressed);
Assert.IsTrue(PacketSuppressor<FMODAssetPacket>.IsSuppressed);
Assert.IsTrue(PacketSuppressor<FMODEventInstancePacket>.IsSuppressed);
Assert.IsTrue(PacketSuppressor<FMODCustomEmitterPacket>.IsSuppressed);
Assert.IsTrue(PacketSuppressor<FMODCustomLoopingEmitterPacket>.IsSuppressed);
Assert.IsTrue(PacketSuppressor<FMODStudioEmitterPacket>.IsSuppressed);
}
Assert.IsFalse(PacketSuppressor<BedEnter>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODAssetPacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODEventInstancePacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODCustomEmitterPacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODCustomLoopingEmitterPacket>.IsSuppressed);
Assert.IsFalse(PacketSuppressor<FMODStudioEmitterPacket>.IsSuppressed);
}
}

View File

@@ -0,0 +1,16 @@
using System;
using NitroxModel.Packets;
namespace Nitrox.Test.Client.Communication
{
[Serializable]
public class TestNonActionPacket : Packet
{
public ushort PlayerId { get; }
public TestNonActionPacket(ushort playerId)
{
PlayerId = playerId;
}
}
}

View File

@@ -0,0 +1,85 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxModel.Logger;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NitroxClient.GameLogic.Helper;
[TestClass]
public class BaseSerializationHelperTest
{
[TestMethod]
public void TestBytesRestoring()
{
List<int> lengths = new() { 10000, 100000, 300000, 500000 };
foreach (int length in lengths)
{
TestSerialization(GenerateRealisticBytes(length));
}
}
[TestMethod]
public void TestAllZeroBytes()
{
List<int> lengths = new() { 10000, 100000, 300000, 500000 };
foreach (int length in lengths)
{
byte[] data = new byte[length];
TestSerialization(data);
}
}
[TestMethod]
public void TestAllMaxBytes()
{
List<int> lengths = new() { 10000, 100000, 300000, 500000 };
foreach (int length in lengths)
{
byte[] data = new byte[length];
data.AsSpan().Fill(byte.MaxValue);
TestSerialization(data);
}
}
/// <summary>
/// Generates bytes to emulate Base data arrays
/// </summary>
private static byte[] GenerateRealisticBytes(int length)
{
byte[] generated = new byte[length];
byte[] randomBytes = new byte[length];
int randomIndex = 0;
Random random = new();
random.NextBytes(randomBytes);
for (int i = 0; i < length; i++)
{
if (random.Next(100) < 3)
{
generated[i] = randomBytes[randomIndex++];
}
else
{
generated[i] = 0;
}
}
return generated;
}
private static void TestSerialization(byte[] original)
{
byte[] compressed = BaseSerializationHelper.CompressBytes(original);
byte[] decompressed = BaseSerializationHelper.DecompressBytes(compressed, original.Length);
Log.Info($"Size: [Original: {original.Length}, Compressed: {compressed.Length}, Decompressed: {decompressed.Length}]");
Log.Info($"Original: {string.Join(", ", original.Take(100))}");
Log.Info($"Compressed: {string.Join(", ", compressed.Take(100))}");
Log.Info($"Decompressed: {string.Join(", ", decompressed.Take(100))}\n");
Assert.AreEqual(original.Length, decompressed.Length);
for (int i = 0; i < original.Length; i++)
{
Assert.AreEqual(original[i], decompressed[i]);
}
}
}

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Client.Communication.MultiplayerSession;
using NitroxModel.MultiplayerSession;
using NitroxModel_Subnautica.DataStructures;
using NSubstitute;
using UnityEngine;
namespace NitroxClient.GameLogic.PlayerLogic.PlayerPreferences
{
[TestClass]
public class PlayerPreferenceManagerTests
{
[TestMethod]
public void ShouldBeAbleToRetrieveANewPreferenceEntry()
{
//Given
PlayerPreferenceState playerPreferenceState = new PlayerPreferenceState();
playerPreferenceState.Preferences = new Dictionary<string, PlayerPreference>();
IPreferenceStateProvider stateProvider = Substitute.For<IPreferenceStateProvider>();
stateProvider.GetPreferenceState().Returns(playerPreferenceState);
PlayerPreferenceManager playerPreferenceManager = new PlayerPreferenceManager(stateProvider);
PlayerPreference playerPreference = new PlayerPreference(TestConstants.TEST_PLAYER_NAME, RandomColorGenerator.GenerateColor().ToUnity());
Color preferredColor = playerPreference.PreferredColor();
//When
playerPreferenceManager.SetPreference(TestConstants.TEST_IP_ADDRESS, playerPreference);
PlayerPreference result = playerPreferenceManager.GetPreference(TestConstants.TEST_IP_ADDRESS);
//Then
result.PlayerName.Should().Be(TestConstants.TEST_PLAYER_NAME);
result.RedAdditive.Should().Be(preferredColor.r);
result.GreenAdditive.Should().Be(preferredColor.g);
result.BlueAdditive.Should().Be(preferredColor.b);
}
[TestMethod]
public void ShouldBeAbleToRetrieveUpdatedPreferencesForAnExistingIpAddress()
{
//Given
PlayerPreferenceState playerPreferenceState = new PlayerPreferenceState();
playerPreferenceState.Preferences = new Dictionary<string, PlayerPreference>();
IPreferenceStateProvider stateProvider = Substitute.For<IPreferenceStateProvider>();
stateProvider.GetPreferenceState().Returns(playerPreferenceState);
PlayerPreferenceManager playerPreferenceManager = new PlayerPreferenceManager(stateProvider);
PlayerPreference playerPreference = new PlayerPreference(TestConstants.TEST_PLAYER_NAME, RandomColorGenerator.GenerateColor().ToUnity());
Color newColor = RandomColorGenerator.GenerateColor().ToUnity();
PlayerPreference newPlayerPreference = new PlayerPreference(TestConstants.TEST_PLAYER_NAME, newColor);
//When
playerPreferenceManager.SetPreference(TestConstants.TEST_IP_ADDRESS, playerPreference);
playerPreferenceManager.SetPreference(TestConstants.TEST_IP_ADDRESS, newPlayerPreference);
PlayerPreference result = playerPreferenceManager.GetPreference(TestConstants.TEST_IP_ADDRESS);
//Then
result.RedAdditive.Should().Be(newColor.r);
result.GreenAdditive.Should().Be(newColor.g);
result.BlueAdditive.Should().Be(newColor.b);
}
[TestMethod]
public void SetPreferenceShouldThrowExceptionWhenGivenANullIpAddress()
{
//Arrange
PlayerPreferenceState playerPreferenceState = new PlayerPreferenceState();
playerPreferenceState.Preferences = new Dictionary<string, PlayerPreference>();
IPreferenceStateProvider stateProvider = Substitute.For<IPreferenceStateProvider>();
stateProvider.GetPreferenceState().Returns(playerPreferenceState);
PlayerPreferenceManager playerPreferenceManager = new PlayerPreferenceManager(stateProvider);
PlayerPreference playerPreference = new PlayerPreference(TestConstants.TEST_PLAYER_NAME, RandomColorGenerator.GenerateColor().ToUnity());
//Act
Action action = () => playerPreferenceManager.SetPreference(null, playerPreference);
//Assert
action.Should().Throw<ArgumentNullException>();
}
[TestMethod]
public void SetPreferenceShouldThrowExceptionWhenGivenANullJoinServerSettingsReference()
{
//Arrange
PlayerPreferenceState playerPreferenceState = new PlayerPreferenceState();
playerPreferenceState.Preferences = new Dictionary<string, PlayerPreference>();
IPreferenceStateProvider stateProvider = Substitute.For<IPreferenceStateProvider>();
stateProvider.GetPreferenceState().Returns(playerPreferenceState);
PlayerPreferenceManager playerPreferenceManager = new PlayerPreferenceManager(stateProvider);
//Act
Action action = () => playerPreferenceManager.SetPreference(TestConstants.TEST_IP_ADDRESS, null);
//Assert
action.Should().Throw<ArgumentNullException>();
}
[TestMethod]
public void ShouldGetTheLastSetPlayerPreferenceWhenJoiningANewServer()
{
//Given
PlayerPreferenceState playerPreferenceState = new PlayerPreferenceState();
playerPreferenceState.Preferences = new Dictionary<string, PlayerPreference>();
IPreferenceStateProvider stateProvider = Substitute.For<IPreferenceStateProvider>();
stateProvider.GetPreferenceState().Returns(playerPreferenceState);
PlayerPreferenceManager playerPreferenceManager = new PlayerPreferenceManager(stateProvider);
PlayerPreference firstPreference = new PlayerPreference(TestConstants.TEST_PLAYER_NAME, RandomColorGenerator.GenerateColor().ToUnity());
string firstIpAddress = "127.0.0.1";
playerPreferenceManager.SetPreference(firstIpAddress, firstPreference);
PlayerPreference secondPreference = new PlayerPreference(TestConstants.TEST_PLAYER_NAME, RandomColorGenerator.GenerateColor().ToUnity());
string secondIpAddress = "123.456.789.321";
playerPreferenceManager.SetPreference(secondIpAddress, secondPreference);
PlayerPreference thirdPreference = new PlayerPreference(TestConstants.TEST_PLAYER_NAME, RandomColorGenerator.GenerateColor().ToUnity());
Color expectedColor = thirdPreference.PreferredColor();
string thirdIpAddress = "000.000.000.000";
playerPreferenceManager.SetPreference(thirdIpAddress, thirdPreference);
//When
PlayerPreference result = playerPreferenceManager.GetPreference(TestConstants.TEST_IP_ADDRESS);
//Then
result.PlayerName.Should().Be(thirdPreference.PlayerName);
result.RedAdditive.Should().Be(expectedColor.r);
result.GreenAdditive.Should().Be(expectedColor.g);
result.BlueAdditive.Should().Be(expectedColor.b);
}
[TestMethod]
public void ShouldBeAbleToRetrieveADefaultPreferenceWhenThePlayerHasNone()
{
//Given
PlayerPreferenceState playerPreferenceState = new PlayerPreferenceState();
playerPreferenceState.Preferences = new Dictionary<string, PlayerPreference>();
IPreferenceStateProvider stateProvider = Substitute.For<IPreferenceStateProvider>();
stateProvider.GetPreferenceState().Returns(playerPreferenceState);
PlayerPreferenceManager playerPreferenceManager = new PlayerPreferenceManager(stateProvider);
//When
PlayerPreference result = playerPreferenceManager.GetPreference(TestConstants.TEST_IP_ADDRESS);
//Then
result.Should().NotBeNull();
result.PlayerName.Should().BeNullOrEmpty();
}
[TestMethod]
public void GetPreferenceShouldThrowExceptionWhenGivenNullIpAddress()
{
//Arrange
PlayerPreferenceState playerPreferenceState = new PlayerPreferenceState();
playerPreferenceState.Preferences = new Dictionary<string, PlayerPreference>();
IPreferenceStateProvider stateProvider = Substitute.For<IPreferenceStateProvider>();
stateProvider.GetPreferenceState().Returns(playerPreferenceState);
PlayerPreferenceManager playerPreferenceManager = new PlayerPreferenceManager(stateProvider);
//Act
Action action = () => playerPreferenceManager.GetPreference(null);
//Assert
action.Should().Throw<ArgumentNullException>();
}
}
}

View File

@@ -0,0 +1,10 @@
global using static FluentAssertions.FluentActions;
global using FluentAssertions;
global using Microsoft.VisualStudio.TestTools.UnitTesting;
global using System;
global using System.Collections;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Reflection;
global using System.Threading;

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Nitrox.Test.Helper;
public static class AssertHelper
{
public static void IsListEqual<TSource>(IOrderedEnumerable<TSource> first, IOrderedEnumerable<TSource> second, Action<TSource, TSource> assertComparer)
{
Assert.IsNotNull(first);
Assert.IsNotNull(second);
List<TSource> firstList = first.ToList();
List<TSource> secondList = second.ToList();
Assert.AreEqual(firstList.Count, secondList.Count);
for (int index = 0; index < firstList.Count; index++)
{
assertComparer(firstList[index], secondList[index]);
}
}
public static void IsDictionaryEqual<TKey, TValue>(IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second)
{
Assert.IsNotNull(first);
Assert.IsNotNull(second);
Assert.AreEqual(first.Count, second.Count);
for (int index = 0; index < first.Count; index++)
{
KeyValuePair<TKey, TValue> firstKeyValuePair = first.ElementAt(index);
Assert.IsTrue(second.TryGetValue(firstKeyValuePair.Key, out TValue secondValue), $"Second dictionary didn't contain {firstKeyValuePair.Key}");
Assert.AreEqual(firstKeyValuePair.Value, secondValue, $"Values didn't match with the same key: {firstKeyValuePair.Key}");
}
}
public static void IsDictionaryEqual<TKey, TValue>(IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second, Action<KeyValuePair<TKey, TValue>, KeyValuePair<TKey, TValue>> assertComparer)
{
Assert.IsNotNull(first);
Assert.IsNotNull(second);
Assert.AreEqual(first.Count, second.Count);
for (int index = 0; index < first.Count; index++)
{
KeyValuePair<TKey, TValue> firstKeyValuePair = first.ElementAt(index);
Assert.IsTrue(second.TryGetValue(firstKeyValuePair.Key, out TValue secondValue), $"Second dictionary didn't contain {firstKeyValuePair.Key}");
assertComparer(firstKeyValuePair, new KeyValuePair<TKey, TValue>(firstKeyValuePair.Key, secondValue));
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NitroxModel_Subnautica.Logger;
using NitroxModel.Packets;
using NitroxModel.Packets.Processors.Abstract;
using NitroxServer;
using NitroxServer_Subnautica;
using NitroxServer.ConsoleCommands.Abstract;
namespace Nitrox.Test.Helper.Faker;
public class NitroxAbstractFaker : NitroxFaker, INitroxFaker
{
private static readonly Dictionary<Type, Type[]> subtypesByBaseType;
static NitroxAbstractFaker()
{
Assembly[] assemblies = { typeof(Packet).Assembly, typeof(SubnauticaInGameLogger).Assembly, typeof(ServerAutoFacRegistrar).Assembly, typeof(SubnauticaServerAutoFacRegistrar).Assembly };
HashSet<Type> blacklistedTypes = new() { typeof(Packet), typeof(CorrelatedPacket), typeof(Command), typeof(PacketProcessor) };
List<Type> types = new();
foreach (Assembly assembly in assemblies)
{
types.AddRange(assembly.GetTypes());
}
subtypesByBaseType = types.Where(type => type.IsAbstract && !type.IsSealed && !blacklistedTypes.Contains(type))
.ToDictionary(type => type, type => types.Where(t => type.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface).ToArray())
.Where(dict => dict.Value.Length > 0)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
public readonly int AssignableTypesCount;
private readonly Queue<INitroxFaker> assignableFakers = new();
public NitroxAbstractFaker(Type type)
{
if (!type.IsAbstract)
{
throw new ArgumentException("Argument is not abstract", nameof(type));
}
if (!subtypesByBaseType.TryGetValue(type, out Type[] subTypes))
{
throw new ArgumentException($"Argument is not contained in {nameof(subtypesByBaseType)}", nameof(type));
}
OutputType = type;
AssignableTypesCount = subTypes.Length;
FakerByType.Add(type, this);
foreach (Type subType in subTypes)
{
assignableFakers.Enqueue(GetOrCreateFaker(subType));
}
}
public INitroxFaker[] GetSubFakers() => assignableFakers.ToArray();
/// <summary>
/// Selects an implementing type in a round-robin fashion of the abstract type of this faker. Then creates an instance of it.
/// </summary>
public object GenerateUnsafe(HashSet<Type> typeTree)
{
INitroxFaker assignableFaker = assignableFakers.Dequeue();
assignableFakers.Enqueue(assignableFaker);
return assignableFaker.GenerateUnsafe(typeTree);
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace Nitrox.Test.Helper.Faker;
public class NitroxActionFaker : NitroxFaker, INitroxFaker
{
private readonly Func<Bogus.Faker, object> generateAction;
public NitroxActionFaker(Type type, Func<Bogus.Faker, object> action)
{
OutputType = type;
generateAction = action;
}
public INitroxFaker[] GetSubFakers() => Array.Empty<INitroxFaker>();
public object GenerateUnsafe(HashSet<Type> _) => generateAction.Invoke(Faker);
}

View File

@@ -0,0 +1,239 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using BinaryPack.Attributes;
using NitroxModel.Logger;
namespace Nitrox.Test.Helper.Faker;
public class NitroxAutoFaker<T> : NitroxFaker, INitroxFaker
{
private readonly ConstructorInfo constructor;
private readonly MemberInfo[] memberInfos;
private readonly INitroxFaker[] parameterFakers;
public NitroxAutoFaker()
{
Type type = typeof(T);
if (!IsValidType(type))
{
throw new InvalidOperationException($"{type.Name} is not a valid type for {nameof(NitroxAutoFaker<T>)}");
}
OutputType = type;
FakerByType.Add(type, this);
if (type.GetCustomAttributes(typeof(DataContractAttribute), false).Length > 0)
{
memberInfos = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(member => member.GetCustomAttributes<DataMemberAttribute>().Any()).ToArray();
}
else
{
memberInfos = type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.Where(member => member.MemberType is MemberTypes.Field or MemberTypes.Property &&
!member.GetCustomAttributes<IgnoredMemberAttribute>().Any())
.ToArray();
}
if (!TryGetConstructorForType(type, memberInfos, out constructor) &&
!TryGetConstructorForType(type, Array.Empty<MemberInfo>(), out constructor))
{
throw new NullReferenceException($"Could not find a constructor with no parameters for {type}");
}
parameterFakers = new INitroxFaker[memberInfos.Length];
Type[] constructorArgumentTypes = constructor.GetParameters().Select(p => p.ParameterType).ToArray();
for (int i = 0; i < memberInfos.Length; i++)
{
Type dataMemberType = constructorArgumentTypes.Length == memberInfos.Length ? constructorArgumentTypes[i] : memberInfos[i].GetMemberType();
if (FakerByType.TryGetValue(dataMemberType, out INitroxFaker memberFaker))
{
parameterFakers[i] = memberFaker;
}
else
{
parameterFakers[i] = CreateFaker(dataMemberType);
}
}
}
private void ValidateFakerTree()
{
List<INitroxFaker> fakerTree = new();
void ValidateFaker(INitroxFaker nitroxFaker)
{
if (fakerTree.Contains(nitroxFaker))
{
return;
}
fakerTree.Add(nitroxFaker);
if (nitroxFaker is NitroxAbstractFaker abstractFaker)
{
NitroxCollectionFaker collectionFaker = (NitroxCollectionFaker)fakerTree.LastOrDefault(f => f.GetType() == typeof(NitroxCollectionFaker));
if (collectionFaker != null)
{
collectionFaker.GenerateSize = Math.Max(collectionFaker.GenerateSize, abstractFaker.AssignableTypesCount);
}
}
foreach (INitroxFaker subFaker in nitroxFaker.GetSubFakers())
{
ValidateFaker(subFaker);
}
fakerTree.Remove(nitroxFaker);
}
foreach (INitroxFaker parameterFaker in parameterFakers)
{
ValidateFaker(parameterFaker);
}
}
public INitroxFaker[] GetSubFakers() => parameterFakers;
public T Generate()
{
ValidateFakerTree();
return (T)GenerateUnsafe(new HashSet<Type>());
}
public object GenerateUnsafe(HashSet<Type> typeTree)
{
object[] parameterValues = new object[parameterFakers.Length];
for (int i = 0; i < parameterValues.Length; i++)
{
INitroxFaker parameterFaker = parameterFakers[i];
if (typeTree.Contains(parameterFaker.OutputType))
{
if (parameterFaker is NitroxCollectionFaker collectionFaker)
{
parameterValues[i] = Activator.CreateInstance(collectionFaker.OutputCollectionType);
}
else
{
parameterValues[i] = null;
}
}
else
{
typeTree.Add(parameterFaker.OutputType);
parameterValues[i] = parameterFakers[i].GenerateUnsafe(typeTree);
typeTree.Remove(parameterFaker.OutputType);
}
}
if (constructor.GetParameters().Length == parameterValues.Length)
{
return (T)constructor.Invoke(parameterValues);
}
T obj = (T)constructor.Invoke(Array.Empty<object>());
for (int index = 0; index < memberInfos.Length; index++)
{
MemberInfo memberInfo = memberInfos[index];
switch (memberInfo.MemberType)
{
case MemberTypes.Field:
((FieldInfo)memberInfo).SetValue(obj, parameterValues[index]);
break;
case MemberTypes.Property:
PropertyInfo propertyInfo = (PropertyInfo)memberInfo;
if (!propertyInfo.CanWrite &&
NitroxCollectionFaker.IsCollection(parameterValues[index].GetType(), out NitroxCollectionFaker.CollectionType collectionType))
{
dynamic origColl = propertyInfo.GetValue(obj);
switch (collectionType)
{
case NitroxCollectionFaker.CollectionType.ARRAY:
for (int i = 0; i < ((Array)parameterValues[index]).Length; i++)
{
origColl[i] = ((Array)parameterValues[index]).GetValue(i);
}
break;
case NitroxCollectionFaker.CollectionType.LIST:
case NitroxCollectionFaker.CollectionType.DICTIONARY:
case NitroxCollectionFaker.CollectionType.SET:
foreach (dynamic createdValue in ((IEnumerable)parameterValues[index]))
{
origColl.Add(createdValue);
}
break;
case NitroxCollectionFaker.CollectionType.QUEUE:
foreach (dynamic createdValue in ((IEnumerable)parameterValues[index]))
{
origColl.Enqueue(createdValue);
}
break;
case NitroxCollectionFaker.CollectionType.NONE:
default:
throw new ArgumentOutOfRangeException();
}
}
else if(propertyInfo.CanWrite)
{
propertyInfo.SetValue(obj, parameterValues[index]);
}
else
{
Regex backingFieldNameRegex = new($"\\A<{propertyInfo.Name}>k__BackingField\\Z");
FieldInfo backingField = propertyInfo.DeclaringType.GetRuntimeFields().FirstOrDefault(a => backingFieldNameRegex.IsMatch(a.Name));
if (backingField == null)
{
throw new InvalidOperationException($"{propertyInfo.DeclaringType}.{propertyInfo.Name} is not accessible for writing");
}
backingField.SetValue(obj, parameterValues[index]);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return obj;
}
private static bool TryGetConstructorForType(Type type, MemberInfo[] dataMembers, out ConstructorInfo constructorInfo)
{
foreach (ConstructorInfo constructor in type.GetConstructors())
{
if (constructor.GetParameters().Length != dataMembers.Length)
{
continue;
}
bool parameterValid = constructor.GetParameters()
.All(parameter => dataMembers.Any(d => d.GetMemberType() == parameter.ParameterType &&
d.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase)));
if (parameterValid)
{
constructorInfo = constructor;
return true;
}
}
constructorInfo = null;
return false;
}
}

View File

@@ -0,0 +1,202 @@
using NitroxModel.DataStructures;
namespace Nitrox.Test.Helper.Faker;
public class NitroxCollectionFaker : NitroxFaker, INitroxFaker
{
public enum CollectionType
{
NONE,
ARRAY,
LIST,
DICTIONARY,
SET,
QUEUE
}
private const int DEFAULT_SIZE = 2;
public static bool IsCollection(Type t, out CollectionType collectionType)
{
if (t.IsArray && t.GetArrayRank() == 1)
{
collectionType = CollectionType.ARRAY;
return true;
}
if (t.IsGenericType)
{
Type[] genericInterfacesDefinition = t.GetInterfaces()
.Where(i => i.IsGenericType)
.Select(i => i.GetGenericTypeDefinition())
.ToArray();
if (genericInterfacesDefinition.Any(i => i == typeof(IList<>)))
{
collectionType = CollectionType.LIST;
return true;
}
if (genericInterfacesDefinition.Any(i => i == typeof(IDictionary<,>)))
{
collectionType = CollectionType.DICTIONARY;
return true;
}
if (genericInterfacesDefinition.Any(i => i == typeof(ISet<>)))
{
collectionType = CollectionType.SET;
return true;
}
Type genericTypeDefinition = t.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(Queue<>) || genericTypeDefinition == typeof(ThreadSafeQueue<>)) // Queue has no defining interface
{
collectionType = CollectionType.QUEUE;
return true;
}
}
collectionType = CollectionType.NONE;
return false;
}
public static bool TryGetCollectionTypes(Type type, out Type[] types)
{
if (!IsCollection(type, out CollectionType collectionType))
{
types = [];
return false;
}
if (collectionType == CollectionType.ARRAY)
{
types = [type.GetElementType()];
return true;
}
types = type.GenericTypeArguments;
return true;
}
public int GenerateSize = DEFAULT_SIZE;
public Type OutputCollectionType;
private readonly INitroxFaker[] subFakers;
private readonly Func<HashSet<Type>, object> generateAction;
public NitroxCollectionFaker(Type type, CollectionType collectionType)
{
OutputCollectionType = type;
INitroxFaker elementFaker;
switch (collectionType)
{
case CollectionType.ARRAY:
Type arrayType = OutputType = type.GetElementType();
elementFaker = GetOrCreateFaker(arrayType);
subFakers = [elementFaker];
generateAction = typeTree =>
{
typeTree.Add(arrayType);
Array array = Array.CreateInstance(arrayType, GenerateSize);
for (int i = 0; i < GenerateSize; i++)
{
array.SetValue(elementFaker.GenerateUnsafe(typeTree), i);
}
typeTree.Remove(arrayType);
return array;
};
break;
case CollectionType.LIST:
case CollectionType.SET:
Type listType = OutputType = type.GenericTypeArguments[0];
elementFaker = GetOrCreateFaker(listType);
subFakers = [elementFaker];
generateAction = typeTree =>
{
typeTree.Add(listType);
dynamic list = Activator.CreateInstance(type);
for (int i = 0; i < GenerateSize; i++)
{
MethodInfo castMethod = CastMethodBase.MakeGenericMethod(OutputType);
dynamic castedObject = castMethod.Invoke(null, [elementFaker.GenerateUnsafe(typeTree)]);
list.Add(castedObject);
}
typeTree.Remove(listType);
return list;
};
break;
case CollectionType.DICTIONARY:
Type[] dicType = type.GenericTypeArguments;
OutputType = dicType[1]; // A little hacky but should work as we don't use circular dependencies as keys
INitroxFaker keyFaker = GetOrCreateFaker(dicType[0]);
INitroxFaker valueFaker = GetOrCreateFaker(dicType[1]);
subFakers = [keyFaker, valueFaker];
generateAction = typeTree =>
{
typeTree.Add(dicType[0]);
typeTree.Add(dicType[1]);
IDictionary dict = (IDictionary)Activator.CreateInstance(type);
for (int i = 0; i < GenerateSize; i++)
{
for (int tries = 0; tries < 10; tries++)
{
object key = keyFaker.GenerateUnsafe(typeTree);
if (!dict.Contains(key))
{
dict.Add(key, valueFaker.GenerateUnsafe(typeTree));
break;
}
if (tries == 9)
{
throw new InvalidOperationException($"While generating action for filling Dictionary an unique key of {dicType[0]} couldn't be generated even after 10 tries");
}
}
}
typeTree.Remove(dicType[0]);
typeTree.Remove(dicType[1]);
return dict;
};
break;
case CollectionType.QUEUE:
Type queueType = OutputType = type.GenericTypeArguments[0];
elementFaker = GetOrCreateFaker(queueType);
subFakers = [elementFaker];
generateAction = typeTree =>
{
typeTree.Add(queueType);
dynamic queue = Activator.CreateInstance(type);
for (int i = 0; i < GenerateSize; i++)
{
MethodInfo castMethod = CastMethodBase.MakeGenericMethod(OutputType);
dynamic castedObject = castMethod.Invoke(null, [elementFaker.GenerateUnsafe(typeTree)]);
queue!.Enqueue(castedObject);
}
typeTree.Remove(queueType);
return queue;
};
break;
case CollectionType.NONE:
default:
throw new ArgumentOutOfRangeException(nameof(collectionType), collectionType, null);
}
}
public INitroxFaker[] GetSubFakers() => subFakers;
public object GenerateUnsafe(HashSet<Type> typeTree) => generateAction.Invoke(typeTree);
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
namespace Nitrox.Test.Helper.Faker;
public interface INitroxFaker
{
public Type OutputType { get; }
public INitroxFaker[] GetSubFakers();
public object GenerateUnsafe(HashSet<Type> typeTree);
}
public abstract class NitroxFaker
{
public Type OutputType { get; protected init; }
protected static readonly Bogus.Faker Faker;
static NitroxFaker()
{
Faker = new Bogus.Faker();
}
protected static readonly Dictionary<Type, INitroxFaker> FakerByType = new()
{
// Basic types
{ typeof(bool), new NitroxActionFaker(typeof(bool), f => f.Random.Bool()) },
{ typeof(byte), new NitroxActionFaker(typeof(byte), f => f.Random.Byte()) },
{ typeof(sbyte), new NitroxActionFaker(typeof(sbyte), f => f.Random.SByte()) },
{ typeof(short), new NitroxActionFaker(typeof(short), f => f.Random.Short()) },
{ typeof(ushort), new NitroxActionFaker(typeof(ushort), f => f.Random.UShort()) },
{ typeof(int), new NitroxActionFaker(typeof(int), f => f.Random.Int()) },
{ typeof(uint), new NitroxActionFaker(typeof(uint), f => f.Random.UInt()) },
{ typeof(long), new NitroxActionFaker(typeof(long), f => f.Random.Long()) },
{ typeof(ulong), new NitroxActionFaker(typeof(ulong), f => f.Random.ULong()) },
{ typeof(decimal), new NitroxActionFaker(typeof(decimal), f => f.Random.Decimal()) },
{ typeof(float), new NitroxActionFaker(typeof(float), f => f.Random.Float()) },
{ typeof(double), new NitroxActionFaker(typeof(double), f => f.Random.Double()) },
{ typeof(char), new NitroxActionFaker(typeof(char), f => f.Random.Char()) },
{ typeof(string), new NitroxActionFaker(typeof(string), f => f.Random.Word()) },
// Nitrox types
{ typeof(NitroxTechType), new NitroxActionFaker(typeof(NitroxTechType), f => new NitroxTechType(f.PickRandom<TechType>().ToString())) },
{ typeof(NitroxId), new NitroxActionFaker(typeof(NitroxId), f => new NitroxId(f.Random.Guid())) },
};
public static INitroxFaker GetOrCreateFaker(Type t)
{
return FakerByType.TryGetValue(t, out INitroxFaker nitroxFaker) ? nitroxFaker : CreateFaker(t);
}
protected static INitroxFaker CreateFaker(Type type)
{
if (type.IsAbstract)
{
return new NitroxAbstractFaker(type);
}
if (type.IsEnum)
{
return new NitroxActionFaker(type, f =>
{
string[] selection = Enum.GetNames(type);
if (selection.Length == 0)
{
throw new ArgumentException("There are no enum values after exclusion to choose from.");
}
string val = f.Random.ArrayElement(selection);
return Enum.Parse(type, val);
});
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>))
{
return new NitroxOptionalFaker(type);
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return new NitroxNullableFaker(type);
}
if (NitroxCollectionFaker.IsCollection(type, out NitroxCollectionFaker.CollectionType collectionType))
{
return new NitroxCollectionFaker(type, collectionType);
}
ConstructorInfo constructor = typeof(NitroxAutoFaker<>).MakeGenericType(type).GetConstructor(Array.Empty<Type>());
if (constructor == null)
{
throw new NullReferenceException($"Could not get generic constructor for {type}");
}
return (INitroxFaker)constructor.Invoke(Array.Empty<object>());
}
protected static bool IsValidType(Type type)
{
return FakerByType.ContainsKey(type) ||
type.GetCustomAttributes(typeof(DataContractAttribute), false).Length >= 1 ||
type.GetCustomAttributes(typeof(SerializableAttribute), false).Length >= 1 ||
(NitroxCollectionFaker.TryGetCollectionTypes(type, out Type[] collectionTypes) && collectionTypes.All(IsValidType)) ||
type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
protected static readonly MethodInfo CastMethodBase = typeof(NitroxFaker).GetMethod(nameof(Cast), BindingFlags.NonPublic | BindingFlags.Static);
protected static T Cast<T>(object o)
{
return (T)o;
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Nitrox.Test.Helper.Faker;
public class NitroxNullableFaker : NitroxFaker, INitroxFaker
{
private readonly Func<HashSet<Type>, object> generateAction;
public NitroxNullableFaker(Type type)
{
OutputType = type.GenericTypeArguments[0];
generateAction = (typeTree) =>
{
MethodInfo castMethod = CastMethodBase.MakeGenericMethod(OutputType);
object castedObject = castMethod.Invoke(null, new[] { GetOrCreateFaker(OutputType).GenerateUnsafe(typeTree) });
Type nullableType = typeof(Nullable<>).MakeGenericType(OutputType);
return Activator.CreateInstance(nullableType, castedObject);
};
}
public INitroxFaker[] GetSubFakers() => new[] { GetOrCreateFaker(OutputType) };
public object GenerateUnsafe(HashSet<Type> typeTree) => generateAction.Invoke(typeTree);
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using NitroxModel.DataStructures.Util;
namespace Nitrox.Test.Helper.Faker;
public class NitroxOptionalFaker : NitroxFaker, INitroxFaker
{
private readonly Func<HashSet<Type>, object> generateAction;
public NitroxOptionalFaker(Type type)
{
OutputType = type.GenericTypeArguments[0];
generateAction = (typeTree) =>
{
MethodInfo castMethod = CastMethodBase.MakeGenericMethod(OutputType);
object castedObject = castMethod.Invoke(null, new[] { GetOrCreateFaker(OutputType).GenerateUnsafe(typeTree) });
Type optionalType = typeof(Optional<>).MakeGenericType(OutputType);
return Activator.CreateInstance(optionalType, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance, null, new[] { castedObject }, null);
};
}
public INitroxFaker[] GetSubFakers() => new[] { GetOrCreateFaker(OutputType) };
public object GenerateUnsafe(HashSet<Type> typeTree) => generateAction.Invoke(typeTree);
}

View File

@@ -0,0 +1,22 @@
using System;
namespace Nitrox.Test.Helper;
public static class TypeExtension
{
public static bool IsAssignableToGenericType(this Type givenType, Type genericType)
{
if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType)
{
return true;
}
Type givenBaseType = givenType.BaseType;
if (givenBaseType == null)
{
return false;
}
return IsAssignableToGenericType(givenBaseType, genericType);
}
}

View File

@@ -0,0 +1,148 @@
using Autofac;
namespace NitroxModel.Core;
[TestClass]
public class DependencyInjectionTests
{
[TestInitialize]
public void Init()
{
NitroxServiceLocator.InitializeDependencyContainer(new DependencyInjectionTestsAutoFacRegistrar());
NitroxServiceLocator.BeginNewLifetimeScope();
}
[TestMethod]
public void ShouldResolveDependencyPolymorphically()
{
// Arrange
IRootDependency polymorphicallyResolvedDependency = NitroxServiceLocator.LocateService<IRootDependency>();
// Assert
polymorphicallyResolvedDependency.Should().NotBeNull();
polymorphicallyResolvedDependency.Should().BeOfType<RootDependency>();
}
[TestMethod]
public void ShouldResolveConcreteType()
{
// Arrange
DependencyWithRootDependency directConcreteTypeDependency = NitroxServiceLocator.LocateService<DependencyWithRootDependency>();
// Assert
directConcreteTypeDependency.Should().NotBeNull();
directConcreteTypeDependency.RootDependency.Should().NotBeNull();
directConcreteTypeDependency.RootDependency.Should().BeOfType<RootDependency>();
}
[TestMethod]
public void ShouldResolveGenericDependencies()
{
// Arrange
IServicer<ServiceRecipientA> servicerA = NitroxServiceLocator.LocateService<IServicer<ServiceRecipientA>>();
IServicer<ServiceRecipientB> servicerB = NitroxServiceLocator.LocateService<IServicer<ServiceRecipientB>>();
// Assert
servicerA.Should().NotBeNull();
servicerA.Should().BeOfType<ServiceAProvider>();
Invoking(() => servicerA.PerformService(null)).Should().Throw<NotImplementedException>();
servicerB.Should().NotBeNull();
servicerB.Should().BeOfType<ServiceBProvider>();
Invoking(() => servicerB.PerformService(null)).Should().Throw<NotImplementedException>();
}
[TestMethod]
public void ShouldResolveGenericDependenciesFromManuallyConstructedTypeInstances()
{
// Arrange
Type servicerInstanceType = typeof(IServicer<>);
Type recipientAType = typeof(ServiceRecipientA);
Type recipientBType = typeof(ServiceRecipientB);
Type servicerAType = servicerInstanceType.MakeGenericType(recipientAType);
Type servicerBType = servicerInstanceType.MakeGenericType(recipientBType);
// Act
IServicer<ServiceRecipientA> servicerA = (BaseServiceProvider<ServiceRecipientA>)NitroxServiceLocator.LocateService(servicerAType);
IServicer<ServiceRecipientB> servicerB = (BaseServiceProvider<ServiceRecipientB>)NitroxServiceLocator.LocateService(servicerBType);
// Assert
servicerA.Should().NotBeNull();
servicerA.Should().BeOfType<ServiceAProvider>();
Invoking(() => servicerA.PerformService(null)).Should().Throw<NotImplementedException>();
servicerB.Should().NotBeNull();
servicerB.Should().BeOfType<ServiceBProvider>();
Invoking(() => servicerB.PerformService(null)).Should().Throw<NotImplementedException>();
}
private class DependencyInjectionTestsAutoFacRegistrar : IAutoFacRegistrar
{
public void RegisterDependencies(ContainerBuilder containerBuilder)
{
containerBuilder.RegisterType<RootDependency>().As<IRootDependency>();
containerBuilder.RegisterType<DependencyWithRootDependency>();
containerBuilder.RegisterAssemblyTypes(Assembly.GetAssembly(GetType()))
.AsClosedTypesOf(typeof(IServicer<>));
}
}
}
public interface IRootDependency
{
}
public class RootDependency : IRootDependency
{
}
public class DependencyWithRootDependency
{
public IRootDependency RootDependency { get; }
public DependencyWithRootDependency(IRootDependency rootDependency)
{
RootDependency = rootDependency;
}
}
public interface IServiced
{
}
public interface IServicer<T>
where T : IServiced
{
void PerformService(T serviced);
}
public class ServiceRecipientA : IServiced
{
}
public class ServiceRecipientB : IServiced
{
}
public abstract class BaseServiceProvider<TServiced> : IServicer<TServiced>
where TServiced : IServiced
{
public abstract void PerformService(TServiced serviced);
}
public class ServiceAProvider : BaseServiceProvider<ServiceRecipientA>
{
public override void PerformService(ServiceRecipientA serviced)
{
throw new NotImplementedException();
}
}
public class ServiceBProvider : BaseServiceProvider<ServiceRecipientB>
{
public override void PerformService(ServiceRecipientB serviced)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,117 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures;
[TestClass]
public class CircularBufferTest
{
[TestMethod]
public void ShouldLimitSizeToMaxSize()
{
CircularBuffer<string> buffer = new(1);
buffer.Count.Should().Be(0);
buffer.Add("1");
buffer.Count.Should().Be(1);
buffer.Add("2");
buffer.Count.Should().Be(1);
buffer = new CircularBuffer<string>(5);
buffer.Count.Should().Be(0);
buffer.Add("1");
buffer.Count.Should().Be(1);
buffer.Add("2");
buffer.Count.Should().Be(2);
buffer.Add("3");
buffer.Count.Should().Be(3);
buffer.Add("4");
buffer.Count.Should().Be(4);
buffer.Add("5");
buffer.Count.Should().Be(5);
buffer.Add("6");
buffer.Count.Should().Be(5);
}
[TestMethod]
public void ShouldOverwriteOldestItemInBufferWhenCapped()
{
CircularBuffer<string> buffer = new(3);
buffer.Add("1");
buffer[0].Should().Be("1");
buffer.Add("2");
buffer[1].Should().Be("2");
buffer.Add("3");
buffer[2].Should().Be("3");
buffer.Add("4");
buffer[0].Should().Be("4");
buffer.Add("5");
buffer[1].Should().Be("5");
buffer[2].Should().Be("3");
buffer.Add("6");
buffer[2].Should().Be("6");
buffer.Add("7");
buffer.Should().ContainInOrder("7", "5", "6");
}
[TestMethod]
public void ShouldDiscardAddIfCapacityReached()
{
CircularBuffer<string> buffer = new(0);
buffer.Count.Should().Be(0);
buffer.Add("1");
buffer.Count.Should().Be(0);
}
[TestMethod]
public void ShouldBeEmptyWhenCleared()
{
CircularBuffer<string> buffer = new(10);
buffer.Count.Should().Be(0);
buffer.Add("1");
buffer.Add("1");
buffer.Add("1");
buffer.Count.Should().Be(3);
buffer.Clear();
buffer.Count.Should().Be(0);
}
[TestMethod]
public void ShouldGiveLastChanged()
{
CircularBuffer<int> buffer = new(3);
buffer.LastChangedIndex.Should().Be(-1);
buffer.Add(1);
buffer.LastChangedIndex.Should().Be(0);
buffer.Add(2);
buffer.LastChangedIndex.Should().Be(1);
buffer.Add(3);
buffer.LastChangedIndex.Should().Be(2);
buffer.Add(4);
buffer.LastChangedIndex.Should().Be(0);
buffer.Add(5);
buffer.LastChangedIndex.Should().Be(1);
buffer.Add(6);
buffer.LastChangedIndex.Should().Be(2);
buffer.Add(7);
buffer.LastChangedIndex.Should().Be(0);
buffer.Add(8);
buffer.LastChangedIndex.Should().Be(1);
buffer.Clear();
buffer.LastChangedIndex.Should().Be(-1);
}
[TestMethod]
public void ShouldReverseOrderWithNegativeIndex()
{
CircularBuffer<int> buffer = new(6);
buffer.AddRange(1, 2, 3, 4, 5, 6);
buffer[-1].Should().Be(6);
buffer[-2].Should().Be(5);
buffer[-3].Should().Be(4);
buffer[-4].Should().Be(3);
buffer[-5].Should().Be(2);
buffer[-6].Should().Be(1);
buffer[-7].Should().Be(6);
buffer[-8].Should().Be(5);
}
}

View File

@@ -0,0 +1,45 @@
using System;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures;
[TestClass]
public class NitroxIdTest
{
private NitroxId id1;
private NitroxId id2;
[TestMethod]
public void SameGuidEquality()
{
Guid guid = Guid.NewGuid();
id1 = new(guid);
id2 = new(guid);
(id1 == id2).Should().BeTrue();
id1.Equals(id2).Should().BeTrue();
(id1 != id2).Should().BeFalse();
(!id1.Equals(id2)).Should().BeFalse();
}
[TestMethod]
public void NullGuidEquality()
{
id1 = new();
id2 = null;
(id1 == id2).Should().BeFalse();
id1.Equals(id2).Should().BeFalse();
(id1 != id2).Should().BeTrue();
(!id1.Equals(id2)).Should().BeTrue();
}
[TestMethod]
public void BothNullEquality()
{
id1 = id2 = null;
(id1 != id2).Should().BeFalse();
(id1 == id2).Should().BeTrue();
}
}

View File

@@ -0,0 +1,39 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures
{
[TestClass]
public class NitroxInt3Test
{
private NitroxInt3 int3;
[TestInitialize]
public void Setup()
{
int3 = new NitroxInt3(5, 10, 15);
}
[TestMethod]
public void Equals()
{
NitroxInt3 other1 = new NitroxInt3(5, 10, 15);
NitroxInt3 other2 = new NitroxInt3(15, 10, 5);
int3.Equals(other1).Should().BeTrue();
int3.Equals(other2).Should().BeFalse();
}
[TestMethod]
public void Floor()
{
NitroxInt3.Floor(5.1f, 10.4f, 15.5f).Should().Be(int3);
}
[TestMethod]
public void Ceil()
{
NitroxInt3.Ceil(4.1f, 9.4f, 14.5f).Should().Be(int3);
}
}
}

View File

@@ -0,0 +1,29 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures;
[TestClass]
public class NitroxVersionTest
{
[TestMethod]
public void Equals()
{
NitroxVersion a = new(2, 1);
NitroxVersion b = new(1, 15);
NitroxVersion source = new(2, 1);
source.Equals(a).Should().BeTrue();
source.Equals(b).Should().BeFalse();
}
[TestMethod]
public void Compare()
{
NitroxVersion source = new(2, 1);
source.CompareTo(new(2, 1)).Should().Be(0);
source.CompareTo(new(1, 15)).Should().Be(1);
source.CompareTo(new (2, 2)).Should().Be(-1);
source.CompareTo(new (3, 1)).Should().Be(-1);
}
}

View File

@@ -0,0 +1,78 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures
{
using StringPriorityQueue = PriorityQueue<string>;
[TestClass]
public class PriorityQueueTest
{
[TestMethod]
public void SameOrder()
{
StringPriorityQueue queue = new StringPriorityQueue();
queue.Enqueue(0, "First");
queue.Enqueue(0, "Second");
queue.Enqueue(0, "Third");
Assert.AreEqual("First", queue.Dequeue());
Assert.AreEqual("Second", queue.Dequeue());
Assert.AreEqual("Third", queue.Dequeue());
}
[TestMethod]
public void DifferentOrder()
{
StringPriorityQueue queue = new StringPriorityQueue();
queue.Enqueue(3, "First");
queue.Enqueue(2, "Second");
queue.Enqueue(1, "Third");
Assert.AreEqual("First", queue.Dequeue());
Assert.AreEqual("Second", queue.Dequeue());
Assert.AreEqual("Third", queue.Dequeue());
}
[TestMethod]
public void SomeAreSameOrder()
{
StringPriorityQueue queue = new StringPriorityQueue();
queue.Enqueue(2, "First");
queue.Enqueue(2, "Second");
queue.Enqueue(0, "Third");
Assert.AreEqual("First", queue.Dequeue());
Assert.AreEqual("Second", queue.Dequeue());
Assert.AreEqual("Third", queue.Dequeue());
}
[TestMethod]
public void PrioritySanity()
{
StringPriorityQueue queue = new StringPriorityQueue();
queue.Enqueue(2, "Second");
queue.Enqueue(3, "First");
queue.Enqueue(1, "Third");
Assert.AreEqual("First", queue.Dequeue());
Assert.AreEqual("Second", queue.Dequeue());
Assert.AreEqual("Third", queue.Dequeue());
}
[TestMethod]
public void CountSanity()
{
StringPriorityQueue queue = new StringPriorityQueue();
queue.Enqueue(2, "Second");
queue.Enqueue(3, "First");
queue.Enqueue(1, "Third");
Assert.AreEqual(3, queue.Count);
queue.Dequeue();
queue.Dequeue();
Assert.AreEqual(1, queue.Count);
}
}
}

View File

@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures
{
[TestClass]
public class ThreadSafeListTest
{
private ThreadSafeList<string> list;
[TestInitialize]
public void Setup()
{
list = new ThreadSafeList<string>();
for (int i = 0; i < 10; i++)
{
list.Add($"test {i}");
}
}
[TestMethod]
public void Insert()
{
list.Insert(5, "derp");
list[5].Should().Be("derp");
list[0] = "Hello world!";
list[0].Should().Be("Hello world!");
}
[TestMethod]
public void RemoveAt()
{
list.RemoveAt(5);
foreach (string item in list)
{
item.Should().NotBe("test 5");
}
}
[TestMethod]
public void Remove()
{
list.Remove("test 0");
list[0].Should().Be("test 1");
}
[TestMethod]
public void Find()
{
list.Find(s => s == "test 1").Should().Be("test 1");
list.Find(s => s == "tesT 1").Should().BeNull();
list.Find(s => s == "test 1361").Should().BeNull();
}
[TestMethod]
public void ReadAndWriteSimultaneous()
{
int iterations = 500000;
ThreadSafeList<int> comeGetMe = new(iterations);
List<long> countsRead = new();
long addCount = 0;
Random r = new Random();
DoReaderWriter(() =>
{
countsRead.Add(Interlocked.Read(ref addCount));
},
i =>
{
comeGetMe.Add(r.Next());
Interlocked.Increment(ref addCount);
},
iterations);
addCount.Should().Be(iterations);
countsRead.Count.Should().BeGreaterThan(0);
countsRead.Last().Should().Be(iterations);
comeGetMe.Count.Should().Be(iterations);
}
[TestMethod]
public void IterateAndAddSimultaneous()
{
int iterations = 500000;
ThreadSafeList<int> comeGetMe = new(iterations);
long addCount = 0;
long iterationsReadMany = 0;
Random r = new Random();
DoReaderWriter(() =>
{
foreach (int unused in comeGetMe)
{
Interlocked.Increment(ref iterationsReadMany);
}
},
i =>
{
comeGetMe.Add(r.Next());
Interlocked.Increment(ref addCount);
},
iterations);
addCount.Should().Be(iterations);
iterationsReadMany.Should().BePositive();
}
[TestMethod]
public void IterateAndAdd()
{
ThreadSafeList<int> nums = new()
{
1,2,3,4,5
};
foreach (int num in nums)
{
if (num == 3)
{
nums.Add(10);
}
}
nums.Count.Should().Be(6);
nums.Last().Should().Be(10);
}
private void DoReaderWriter(Action reader, Action<int> writer, int iterators)
{
ManualResetEventSlim barrier = new(false);
Thread readerThread = new(() =>
{
while (!barrier.IsSet)
{
reader();
Thread.Yield();
}
// Read one last time after writer finishes
reader();
});
Thread writerThread = new(() =>
{
for (int i = 0; i < iterators; i++)
{
writer(i);
}
barrier.Set();
});
readerThread.Start();
writerThread.Start();
barrier.Wait();
}
}
}

View File

@@ -0,0 +1,56 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures
{
[TestClass]
public class ThreadSafeQueueTest
{
private ThreadSafeQueue<string> queue;
[TestInitialize]
public void Setup()
{
queue = new ThreadSafeQueue<string>();
for (int i = 0; i < 10; i++)
{
queue.Enqueue($"test {i}");
}
}
[TestMethod]
public void Peek()
{
queue.Peek().Should().Be("test 0");
}
[TestMethod]
public void Enqueue()
{
queue.Enqueue("derp");
queue.Count.Should().Be(11);
}
[TestMethod]
public void Dequeue()
{
queue.Dequeue().Should().Be("test 0");
queue.Count.Should().Be(9);
queue.Should().NotContain("test 0");
}
[TestMethod]
public void Clear()
{
queue.Clear();
queue.Count.Should().Be(0);
}
[TestMethod]
public void Contains()
{
queue.Contains("test 5").Should().BeTrue();
queue.Contains("test 11").Should().BeFalse();
}
}
}

View File

@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures
{
[TestClass]
public class ThreadSafeSetTest
{
private ThreadSafeSet<string> set;
[TestInitialize]
public void Setup()
{
set = new ThreadSafeSet<string>();
for (int i = 0; i < 10; i++)
{
set.Add($"test {i}");
}
}
[TestMethod]
public void Remove()
{
set.Should().Contain("test 0");
set.Remove("test 0");
set.Should().NotContain("test 0");
}
[TestMethod]
public void Contains()
{
set.Contains("test 1").Should().BeTrue();
}
[TestMethod]
public void Except()
{
string[] exclude = { "test 0", "test 5", "test 9" };
set.Should().Contain(exclude);
set.ExceptWith(exclude);
set.Should().NotContain(exclude);
}
[TestMethod]
public void ReadAndWriteSimultaneous()
{
int iterations = 500000;
ThreadSafeSet<string> comeGetMe = new();
List<long> countsRead = new();
long addCount = 0;
Random r = new Random();
DoReaderWriter(() =>
{
countsRead.Add(Interlocked.Read(ref addCount));
},
i =>
{
comeGetMe.Add(new string(Enumerable.Repeat(' ', 10).Select(c => (char)r.Next('A', 'Z')).ToArray()));
Interlocked.Increment(ref addCount);
},
iterations);
addCount.Should().Be(iterations);
countsRead.Count.Should().BeGreaterThan(0);
countsRead.Last().Should().Be(iterations);
comeGetMe.Count.Should().Be(iterations);
}
[TestMethod]
public void IterateAndAddSimultaneous()
{
int iterations = 500000;
ThreadSafeSet<int> comeGetMe = new();
long addCount = 0;
long iterationsReadMany = 0;
Random r = new();
DoReaderWriter(() =>
{
foreach (int item in comeGetMe)
{
Interlocked.Increment(ref iterationsReadMany);
}
},
i =>
{
comeGetMe.Add(r.Next());
Interlocked.Increment(ref addCount);
},
iterations);
addCount.Should().Be(iterations);
iterationsReadMany.Should().BePositive();
}
[TestMethod]
public void IterateAndAdd()
{
ThreadSafeSet<int> nums = new()
{
1,2,3,4,5
};
foreach (int num in nums)
{
if (num == 3)
{
nums.Add(10);
}
}
nums.Count.Should().Be(6);
nums.Last().Should().Be(10);
}
private void DoReaderWriter(Action reader, Action<int> writer, int iterators)
{
ManualResetEventSlim barrier = new(false);
Thread readerThread = new(() =>
{
while (!barrier.IsSet)
{
reader();
Thread.Yield();
}
// Read one last time after writer finishes
reader();
});
Thread writerThread = new(() =>
{
for (int i = 0; i < iterators; i++)
{
writer(i);
}
barrier.Set();
});
readerThread.Start();
writerThread.Start();
barrier.Wait();
}
}
}

View File

@@ -0,0 +1,77 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures.Unity
{
[TestClass]
public class NitroxQuaternionTest
{
private NitroxQuaternion defaultVal;
private const float TOLERANCE = 0.0001f;
[TestInitialize]
public void Init()
{
defaultVal = new NitroxQuaternion(0.5682333f, -0.01828304f, -0.5182831f, 0.6388735f);
}
[TestMethod]
public void TestEquality()
{
NitroxQuaternion other1 = new NitroxQuaternion(0.5682333f, -0.01828304f, -0.5182831f, 0.6388735f);
NitroxQuaternion other2 = new NitroxQuaternion(-0.5682333f, 0.01828304f, 0.5182831f, -0.6388735f);
NitroxQuaternion other3 = new NitroxQuaternion(0.5682343f, -0.01828314f, -0.5182841f, 0.6388745f);
defaultVal.Equals(other1, TOLERANCE).Should().BeTrue();
defaultVal.Equals(other2, TOLERANCE).Should().BeTrue();
defaultVal.Equals(other3, TOLERANCE).Should().BeTrue(); //Tolerance to low to detect the difference
(defaultVal == other1).Should().BeTrue();
(defaultVal == other2).Should().BeTrue();
(defaultVal != other3).Should().BeTrue();
}
[TestMethod]
public void TestMultiplication()
{
NitroxQuaternion result1 = defaultVal * new NitroxQuaternion(-10f, 0.5f, 0.004f, 256.1111f);
NitroxQuaternion result2 = new NitroxQuaternion(-10f, 0.5f, 0.004f, 256.1111f) * defaultVal;
NitroxQuaternion expectedResult1 = new NitroxQuaternion(139.4012f, 0.8175055f, -132.6342f, 169.3162f);
NitroxQuaternion expectedResult2 = new NitroxQuaternion(138.8831f, -9.543612f, -132.8368f, 169.3162f);
result1.Equals(expectedResult1, TOLERANCE).Should().BeTrue($"Expected: {expectedResult1} - Found: {result1}");
result2.Equals(expectedResult2, TOLERANCE).Should().BeTrue($"Expected: {expectedResult2} - Found: {result2}");
}
[TestMethod]
public void TestToEuler()
{
NitroxVector3 euler1 = defaultVal.ToEuler();
NitroxVector3 euler2 = new NitroxQuaternion(0.5f, 0.5f, -0.5f, 0.5f).ToEuler();
NitroxVector3 euler3 = new NitroxQuaternion(-0.5f, 0.5f, -0.5f, -0.5f).ToEuler();
NitroxVector3 expectedResult1 = new NitroxVector3(45f, 300f, 255f);
NitroxVector3 expectedResult1Other = new NitroxVector3(104.5108f, 50.75358f, 316.9205f); //defaultVal can be interpreted as both euler :shrug:
NitroxVector3 expectedResult2 = new NitroxVector3(90f, 90f, 0f);
NitroxVector3 expectedResult3 = new NitroxVector3(90f, 270f, 0f);
(euler1.Equals(expectedResult1, TOLERANCE) || euler1.Equals(expectedResult1Other, TOLERANCE)).Should().BeTrue($"Expected: {expectedResult1} or {expectedResult1Other}- Found: {euler1}");
euler2.Equals(expectedResult2, TOLERANCE).Should().BeTrue($"Expected: {expectedResult2} - Found: {euler2}");
euler3.Equals(expectedResult3, TOLERANCE).Should().BeTrue($"Expected: {expectedResult3} - Found: {euler3}");
}
[TestMethod]
public void TestFromEuler()
{
NitroxQuaternion result1 = NitroxQuaternion.FromEuler(new NitroxVector3(45f, 300f, 255f));
NitroxQuaternion result2 = NitroxQuaternion.FromEuler(new NitroxVector3(45f, -60f, 615f));
NitroxQuaternion result3 = NitroxQuaternion.FromEuler(new NitroxVector3(400f, 10f, -0.07f));
NitroxQuaternion result4 = NitroxQuaternion.FromEuler(new NitroxVector3(360f, 0f, -720));
NitroxQuaternion expectedResult3 = new NitroxQuaternion(-0.3406684f, -0.08210776f, 0.03038081f, -0.9360985f);
result1.Equals(defaultVal, TOLERANCE).Should().BeTrue($"Expected: {defaultVal} - Found: {result1}");
result2.Equals(defaultVal, TOLERANCE).Should().BeTrue($"Expected: {defaultVal} - Found: {result2}");
result3.Equals(expectedResult3, TOLERANCE).Should().BeTrue($"Expected: {expectedResult3} - Found: {result3}");
result4.Equals(NitroxQuaternion.Identity, TOLERANCE).Should().BeTrue($"Expected: {NitroxQuaternion.Identity} - Found: {result4}");
}
}
}

View File

@@ -0,0 +1,95 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.DataStructures.Unity
{
[TestClass]
public class NitroxTransformTest
{
private const float TOLERANCE = 0.005f;
private static readonly NitroxTransform root = new NitroxTransform(new NitroxVector3(1, 1, 1), NitroxQuaternion.FromEuler(0, 0, 0), new NitroxVector3(1, 1, 1));
private static readonly NitroxTransform child1 = new NitroxTransform(new NitroxVector3(5, 3, -6), NitroxQuaternion.FromEuler(30, 0, 10), new NitroxVector3(2, 2, 2));
private static readonly NitroxTransform child2 = new NitroxTransform(new NitroxVector3(11, 0, -0.5f), NitroxQuaternion.FromEuler(180, 30, 80), new NitroxVector3(0.5f, 0.5f, 0.5f));
private static readonly NitroxTransform grandchild1 = new NitroxTransform(new NitroxVector3(13, 8, 15), NitroxQuaternion.FromEuler(-50, 5, 10), new NitroxVector3(1, 1, 1));
private static readonly NitroxTransform grandchild2 = new NitroxTransform(new NitroxVector3(3, 18, -5), NitroxQuaternion.FromEuler(-5, 15, 1), new NitroxVector3(10, 10, 10));
[ClassInitialize]
public static void Setup(TestContext testContext)
{
child1.SetParent(root, false);
child2.SetParent(root, false);
grandchild1.SetParent(child1, false);
grandchild2.SetParent(child2, false);
}
[TestMethod]
public void PositionTest()
{
NitroxVector3 rootResult = new NitroxVector3(1, 1, 1);
NitroxVector3 child1Result = new NitroxVector3(6, 4, -5);
NitroxVector3 child2Result = new NitroxVector3(12, 1, 0.5f);
NitroxVector3 grandchild1Result = new NitroxVector3(28.82663f, 6.55587f, 31.11665f);
NitroxVector3 grandchild2Result = new NitroxVector3(5.79976f, -2.040047f, 6.966463f);
root.Position.Equals(rootResult, TOLERANCE).Should().BeTrue($"Expected: {rootResult} Found: {root.Position}");
child1.Position.Equals(child1Result, TOLERANCE).Should().BeTrue($"Expected: {child1Result} Found: {child1.Position}");
child2.Position.Equals(child2Result, TOLERANCE).Should().BeTrue($"Expected: {child2Result} Found: {child2.Position}");
grandchild1.Position.Equals(grandchild1Result, TOLERANCE).Should().BeTrue($"Expected: {grandchild1Result} Found: {grandchild1.Position}");
grandchild2.Position.Equals(grandchild2Result, TOLERANCE).Should().BeTrue($"Expected: {grandchild2Result} Found: {grandchild2.Position}");
}
[TestMethod]
public void RotationTest()
{
NitroxQuaternion rootResult = NitroxQuaternion.FromEuler(0, 0, 0);
NitroxQuaternion child1Result = NitroxQuaternion.FromEuler(30, 0, 10);
NitroxQuaternion child2Result = NitroxQuaternion.FromEuler(8.537737e-07f, 210, 260);
NitroxQuaternion grandchild1Result = NitroxQuaternion.FromEuler(340.0263f, 355.2486f, 21.87437f);
NitroxQuaternion grandchild2Result = NitroxQuaternion.FromEuler(15.60783f, 212.4433f, 261.9936f);
root.Rotation.Equals(rootResult, TOLERANCE).Should().BeTrue($"Expected: {rootResult} Found: {root.Rotation}");
child1.Rotation.Equals(child1Result, TOLERANCE).Should().BeTrue($"Expected: {child1Result} Found: {child1.Rotation}");
child2.Rotation.Equals(child2Result, TOLERANCE).Should().BeTrue($"Expected: {child2Result} Found: {child2.Rotation}");
grandchild1.Rotation.Equals(grandchild1Result, TOLERANCE).Should().BeTrue($"Expected: {grandchild1Result} Found: {grandchild1.Rotation}");
grandchild2.Rotation.Equals(grandchild2Result, TOLERANCE).Should().BeTrue($"Expected: {grandchild2Result} Found: {grandchild2.Rotation}");
}
[TestMethod]
public void ChangingTransformTest()
{
NitroxVector3 beforeLocalPosition = new NitroxVector3(15, 2, -7);
NitroxQuaternion beforeRotation = NitroxQuaternion.FromEuler(45, 15, 1);
NitroxVector3 beforeChildLocalPosition = new NitroxVector3(75, -10, 13.333f);
NitroxQuaternion beforeChildRotation = NitroxQuaternion.FromEuler(75, 11, 5);
NitroxVector3 setGlobalPosition = new NitroxVector3(34.62131f, 45.99337f, -10.77733f);
NitroxQuaternion setGlobalRotation = new NitroxQuaternion(0.6743798f, 0.2302919f, 0.02638498f, 0.7010573f);
NitroxVector3 afterLocalPosition = new NitroxVector3(17, 14, -13);
NitroxQuaternion afterLocalRotation = NitroxQuaternion.FromEuler(60, 25, 0);
NitroxVector3 afterChildGlobalPosition = new NitroxVector3(379.541f, 109.6675f, -167.4466f);
NitroxQuaternion afterChildGlobalRotation = NitroxQuaternion.FromEuler(17.81689f, 187.7957f, 151.9425f);
NitroxTransform grandchild3 = new NitroxTransform(beforeLocalPosition, beforeRotation, new NitroxVector3(2.5f, 2.5f, 2.5f));
NitroxTransform grandgrandchild1 = new NitroxTransform(beforeChildLocalPosition, beforeChildRotation, new NitroxVector3(1, 1, 1));
grandgrandchild1.SetParent(grandchild3, false);
grandchild3.SetParent(child1, true);
grandchild3.Position.Equals(beforeLocalPosition, TOLERANCE).Should().BeTrue($"Expected: {beforeLocalPosition} Found: {grandchild3.Position}");
grandchild3.Rotation.Equals(beforeRotation, TOLERANCE).Should().BeTrue($"Expected: {beforeRotation} Found: {grandchild3.Rotation}");
grandchild3.Position = setGlobalPosition;
grandchild3.Rotation = setGlobalRotation;
grandchild3.LocalPosition.Equals(afterLocalPosition, TOLERANCE).Should().BeTrue($"Expected: {afterLocalPosition} Found: {grandchild3.LocalPosition}");
grandchild3.LocalRotation.Equals(afterLocalRotation, TOLERANCE).Should().BeTrue($"Expected: {afterLocalRotation} Found: {grandchild3.LocalRotation}");
grandgrandchild1.Position.Equals(afterChildGlobalPosition, TOLERANCE).Should().BeTrue($"Expected: {afterChildGlobalPosition} Found: {grandgrandchild1.Position}");
grandgrandchild1.Rotation.Equals(afterChildGlobalRotation, TOLERANCE).Should().BeTrue($"Expected: {afterChildGlobalRotation} Found: {grandgrandchild1.Rotation}");
}
}
}

View File

@@ -0,0 +1,140 @@
namespace NitroxModel.DataStructures.Util;
[TestClass]
public class OptionalTest
{
/// <summary>
/// These optional additions should be in <see cref="OptionalHasValueDynamicChecks"/> test method but MSTest
/// reuses instances which causes <see cref="Optional{T}.HasValue">Optional{T}.HasValue</see> to be called before the new conditions are added.
/// </summary>
[ClassInitialize]
public static void Init(TestContext _)
{
Optional.ApplyHasValueCondition<Base>(v => v.GetType() == typeof(A) || v.Threshold > 200); // Cheat: allow check if type A to do more complex tests on Optional<T>.HasValue
Optional.ApplyHasValueCondition<A>(v => v.Threshold <= 200);
}
[TestMethod]
public void OptionalGet()
{
Optional<string> op = Optional.Of("test");
op.Value.Should().Be("test");
}
[TestMethod]
public void OptionalIsPresent()
{
Optional<string> op = Optional.Of("test");
op.HasValue.Should().BeTrue();
}
[TestMethod]
public void OptionalIsNotPresent()
{
Optional<string> op = Optional.Empty;
op.HasValue.Should().BeFalse();
}
[TestMethod]
public void OptionalOrElseValidValue()
{
Optional<string> op = Optional.Of("test");
op.OrElse("test2").Should().Be("test");
}
[TestMethod]
public void OptionalOrElseNoValue()
{
Optional<string> op = Optional.Empty;
op.OrElse("test").Should().Be("test");
}
[TestMethod]
public void OptionalEmpty()
{
Optional<string> op = Optional.Empty;
op.HasValue.Should().BeFalse();
}
[TestMethod]
public void OptionalSetValueNull()
{
Optional<Random> op = Optional.Of(new Random());
Assert.IsTrue(op.HasValue);
Assert.ThrowsException<ArgumentNullException>(() => { op = null; }, "Setting optional to null should not be allowed.");
op = Optional.Empty;
Assert.IsFalse(op.HasValue);
}
[TestMethod]
public void OptionalHasValueDynamicChecks()
{
Optional<Base> opBase = Optional.Of(new Base());
opBase.HasValue.Should().BeTrue();
opBase.Value.Threshold.Should().Be(202);
Optional<A> a = Optional.Of(new A());
a.HasValue.Should().BeTrue();
Optional<A> actuallyB = Optional.Of<A>(new B());
actuallyB.HasValue.Should().BeFalse();
Optional<B> b = Optional.Of(new B());
b.HasValue.Should().BeFalse();
// A check should still happen on Base because Optional<Base> includes more-specific-than-itself checks.
Optional<Base> aAsBase = Optional<Base>.Of((Base)a);
aAsBase.HasValue.Should().BeTrue();
aAsBase.Value.Threshold.Should().Be(200);
// Optional<object> should always do all checks because anything can be in it.
// Note: This test can fail if Optional.ApplyHasValueCondition isn't called early enough. Run this test method directly and it should work.
Optional<object> bAsObj = Optional<object>.Of(new B());
bAsObj.HasValue.Should().BeFalse();
// Type C inheritance doesn't allow for type A. But Optional<object> has the check on A. It should skip the A check on C because inheritance doesn't match up.
Optional<object> cAsObj = Optional<object>.Of(new C());
cAsObj.HasValue.Should().BeTrue();
((C)cAsObj.Value).Threshold.Should().Be(203);
}
[TestMethod]
public void OptionalEqualsCheck()
{
Optional<string> op = Optional.OfNullable<string>(null);
Optional<string> op1 = Optional.OfNullable<string>(null);
Optional<string> op2 = Optional.Of("Test");
Optional<string> op3 = Optional.Of("Test2");
Optional<string> op4 = Optional.Of("Test");
Assert.IsFalse(op.Equals(op2));
Assert.IsFalse(op.Equals(op3));
Assert.IsFalse(op2.Equals(op3));
Assert.IsTrue(op.Equals(op1));
Assert.IsTrue(op2.Equals(op4));
Assert.IsTrue(op != op2);
Assert.IsTrue(op2 == op4);
}
private class Base
{
public virtual int Threshold => 202;
}
private class A : Base
{
public override int Threshold => 200;
}
private class B : A
{
public override int Threshold => 201;
}
private class C : Base
{
public override int Threshold => 203;
}
}

View File

@@ -0,0 +1,113 @@
namespace NitroxModel;
[TestClass]
public class ExtensionsTest
{
[TestMethod]
public void RemoveAllFast_ShouldDoNothingWhenEmptyList()
{
object[] list = [];
list.Should().BeEmpty();
list.RemoveAllFast((object)null, static (_, _) => true);
list.Should().BeEmpty();
}
[TestMethod]
public void RemoveAllFast_ShouldDoNothingWhenAlwaysFalsePredicate()
{
List<string> list = ["one", "two", "three", "four"];
list.RemoveAllFast((object)null, static (_, _) => false);
list.Should().BeEquivalentTo("one", "two", "three", "four");
}
[TestMethod]
public void RemoveAllFast_ThrowsErrorIfFixedSizeList()
{
string[] list = ["one", "two", "three"];
Assert.ThrowsException<NotSupportedException>(() => list.RemoveAllFast((object)null, static (item, _) => item == "one"));
}
[TestMethod]
public void RemoveAllFast_CanRemoveFirstItem()
{
List<string> list = ["one", "two", "three"];
list.RemoveAllFast((object)null, static (item, _) => item == "one");
list.Should().BeEquivalentTo("two", "three");
}
[TestMethod]
public void RemoveAllFast_CanRemoveMidItems()
{
List<string> list = ["one", "two", "three", "four"];
list.RemoveAllFast((object)null, static (item, _) => item == "two" || item == "three");
list.Should().BeEquivalentTo("one", "four");
}
[TestMethod]
public void RemoveAllFast_CanRemoveEndItem()
{
List<string> list = ["one", "two", "three", "four"];
list.RemoveAllFast((object)null, static (item, _) => item == "four");
list.Should().BeEquivalentTo("one", "two", "three");
}
[TestMethod]
public void RemoveAllFast_CanRemoveAllItems()
{
List<string> list = ["one", "two", "three", "four"];
list.RemoveAllFast((object)null, static (_, _) => true);
list.Should().BeEmpty();
}
[TestMethod]
public void RemoveAllFast_CanRemoveItemsWithExtraParameterInPredicate()
{
List<string> list = ["one", "two", "three", "four"];
list.RemoveAllFast(3, static (item, length) => item.Length == length);
list.Should().BeEquivalentTo("three", "four");
}
[TestMethod]
public void GetUniqueNonCombinatoryFlags_ShouldReturnUniqueNonCombinatoryFlags()
{
TestEnumFlags.ALL.GetUniqueNonCombinatoryFlags().Should().BeEquivalentTo([TestEnumFlags.A, TestEnumFlags.B, TestEnumFlags.C, TestEnumFlags.D, TestEnumFlags.E, TestEnumFlags.F]);
TestEnumFlags.CDEF.GetUniqueNonCombinatoryFlags().Should().BeEquivalentTo([TestEnumFlags.C, TestEnumFlags.D, TestEnumFlags.E, TestEnumFlags.F]);
TestEnumFlags.E.GetUniqueNonCombinatoryFlags().Should().BeEquivalentTo([TestEnumFlags.E]);
TestEnumFlags.NONE.GetUniqueNonCombinatoryFlags().Should().BeEmpty();
}
[TestMethod]
public void GetUniqueNonCombinatorFlags_ShouldReturnAllUniquesWhenAllBitsSet()
{
((TestEnumFlags)int.MaxValue).GetUniqueNonCombinatoryFlags().Should().BeEquivalentTo([TestEnumFlags.A, TestEnumFlags.B, TestEnumFlags.C, TestEnumFlags.D, TestEnumFlags.E, TestEnumFlags.F]);
}
[TestMethod]
public void GetCommandArgs()
{
Array.Empty<string>().GetCommandArgs("").Should().BeEmpty();
Array.Empty<string>().GetCommandArgs("--something").Should().BeEmpty();
new[] { "/bin/nitrox.dll", "--save", "My World" }.GetCommandArgs("--save").Should().BeEquivalentTo("My World");
new[] { "--nitrox", @"C:\a\path" }.GetCommandArgs("--nitrox").Should().BeEquivalentTo(@"C:\a\path");
new[] { "blabla", "--other=test", "--nitrox", @"C:\a\path" }.GetCommandArgs("--nitrox").Should().BeEquivalentTo(@"C:\a\path");
new[] { "blabla", "--other=test", "--nitrox", @"C:\a\path" }.GetCommandArgs("--other").Should().BeEquivalentTo("test");
new[] { "blabla", "--other=test", "other2", "--nitrox", @"C:\a\path" }.GetCommandArgs("--other").Should().BeEquivalentTo("test");
new[] { "blabla", "--other", "test", "other2", "--nitrox", @"C:\a\path" }.GetCommandArgs("--other").Should().BeEquivalentTo("test", "other2");
}
[Flags]
private enum TestEnumFlags
{
NONE = 0,
F = 1 << 5,
A = 1 << 0,
B = 1 << 1,
C = 1 << 2,
D = 1 << 3,
E = 1 << 4,
AB = A | B,
CD = C | D,
CDEF = CD | E | F,
ALL = AB | CDEF
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NitroxModel.Helper;
[TestClass]
public class KeyValueStoreTest
{
[TestMethod]
public void SetAndReadValue()
{
const string TEST_KEY = "test";
KeyValueStore.Instance.SetValue(TEST_KEY, -50);
Assert.AreEqual(-50, KeyValueStore.Instance.GetValue<int>(TEST_KEY));
KeyValueStore.Instance.SetValue(TEST_KEY, 1337);
Assert.AreEqual(1337, KeyValueStore.Instance.GetValue<int>(TEST_KEY));
// Cleanup
KeyValueStore.Instance.DeleteKey(TEST_KEY);
Assert.IsNull(KeyValueStore.Instance.GetValue<int?>(TEST_KEY));
Assert.IsFalse(KeyValueStore.Instance.KeyExists(TEST_KEY));
}
}

View File

@@ -0,0 +1,68 @@
using System.Net;
using System.Net.Sockets;
namespace NitroxModel.Helper;
[TestClass]
public class NetHelperTest
{
[TestMethod]
public void ShouldMatchPrivateIps()
{
// Tested subnet ranges that are reserved for private networks:
// 10.0.0.0/8
// 127.0.0.0/8
// 172.16.0.0/12
// 192.0.0.0/24
// 192.168.0.0/16
// 198.18.0.0/15
IPAddress.Parse("10.0.0.0").IsPrivate().Should().BeTrue();
IPAddress.Parse("10.0.0.255").IsPrivate().Should().BeTrue();
IPAddress.Parse("172.31.255.255").IsPrivate().Should().BeTrue();
IPAddress.Parse("172.31.255.255").IsPrivate().Should().BeTrue();
IPAddress.Parse("192.0.0.255").IsPrivate().Should().BeTrue();
IPAddress.Parse("192.168.2.1").IsPrivate().Should().BeTrue();
IPAddress.Parse("192.168.2.254").IsPrivate().Should().BeTrue();
IPAddress.Parse("192.168.2.255").IsPrivate().Should().BeTrue();
IPAddress.Parse("198.18.0.1").IsPrivate().Should().BeTrue();
IPAddress.Parse("198.19.255.255").IsPrivate().Should().BeTrue();
IPAddress.Parse("9.255.255.255").IsPrivate().Should().BeFalse();
IPAddress.Parse("91.63.176.12").IsPrivate().Should().BeFalse();
IPAddress.Parse("172.32.0.1").IsPrivate().Should().BeFalse();
IPAddress.Parse("192.0.1.0").IsPrivate().Should().BeFalse();
IPAddress.Parse("198.17.255.255").IsPrivate().Should().BeFalse();
IPAddress.Parse("198.20.0.0").IsPrivate().Should().BeFalse();
}
[TestMethod]
public void ShouldMatchLocalhostIps()
{
IPAddress GetSlightlyDifferentIp(IPAddress address)
{
if (address.AddressFamily != AddressFamily.InterNetwork)
{
throw new Exception("Only supports IPv4");
}
byte[] bytes = address.GetAddressBytes();
unchecked
{
while (bytes[3] is < 1 or > 253)
{
bytes[3]++;
}
bytes[3]++;
}
return new IPAddress(bytes);
}
IPAddress.Parse("127.0.0.1").IsLocalhost().Should().BeTrue();
IPAddress.Parse("127.0.0.2").IsLocalhost().Should().BeTrue();
IPAddress.Parse("192.168.0.255").IsLocalhost().Should().BeFalse();
NetHelper.GetLanIp().IsLocalhost().Should().BeTrue();
IPAddress differentIp = GetSlightlyDifferentIp(NetHelper.GetLanIp());
differentIp.Should().NotBeEquivalentTo(NetHelper.GetLanIp());
differentIp.IsLocalhost().Should().BeFalse();
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Diagnostics;
using System.Reflection;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxClient.MonoBehaviours.Gui.Input;
namespace NitroxModel.Helper
{
[TestClass]
public class ReflectTest
{
[TestMethod]
public void Method()
{
// Get static method.
MethodInfo staticMethod = Reflect.Method(() => AbusedClass.StaticMethodReturnsInt());
staticMethod.Should().NotBeNull();
staticMethod.ReturnType.Should().Be<int>();
staticMethod.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticMethodReturnsInt));
staticMethod.Invoke(null, Array.Empty<object>());
// Extra check for method with parameters, just to be safe.
staticMethod = Reflect.Method(() => AbusedClass.StaticMethodHasParams("", null));
staticMethod.Should().NotBeNull();
staticMethod.ReturnType.Should().Be<string>();
staticMethod.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticMethodHasParams));
staticMethod.GetParameters().Should().OnlyHaveUniqueItems();
staticMethod.GetParameters()[0].Name.Should().BeEquivalentTo("myValue");
staticMethod.GetParameters()[0].ParameterType.Should().Be<string>();
staticMethod.GetParameters()[1].ParameterType.Should().Be<Process>();
staticMethod.Invoke(null, new[] { "hello, reflection", (object)null }).Should().BeEquivalentTo("hello, reflection");
// Get instance method.
MethodInfo instanceMethod = Reflect.Method((AbusedClass t) => t.Method());
instanceMethod.Should().NotBeNull();
instanceMethod.ReturnType.Should().Be<int>();
instanceMethod.Name.Should().BeEquivalentTo(nameof(AbusedClass.Method));
}
[TestMethod]
public void Field()
{
// Get static field.
FieldInfo staticField = Reflect.Field(() => AbusedClass.StaticField);
staticField.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticField));
staticField.FieldType.Should().Be<int>();
// Get instance field.
FieldInfo instanceField = Reflect.Field((AbusedClass t) => t.InstanceField);
instanceField.Name.Should().BeEquivalentTo(nameof(AbusedClass.InstanceField));
instanceField.FieldType.Should().Be<int>();
}
[TestMethod]
public void Property()
{
// Get static property.
PropertyInfo staticProperty = Reflect.Property(() => AbusedClass.StaticProperty);
staticProperty.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticProperty));
staticProperty.PropertyType.Should().Be<int>();
// Get instance property.
PropertyInfo instanceProperty = Reflect.Property((AbusedClass t) => t.InstanceProperty);
instanceProperty.Name.Should().BeEquivalentTo(nameof(AbusedClass.InstanceProperty));
instanceProperty.PropertyType.Should().Be<int>();
}
[TestMethod]
public void Constructor()
{
ConstructorInfo method = Reflect.Constructor(() => new KeyBindingManager());
method.DeclaringType.Should().Be<KeyBindingManager>();
}
private class AbusedClass
{
public static readonly int StaticReadOnlyField = 1;
public static int StaticField = 2;
public int InstanceField = 3;
public static int StaticProperty { get; set; } = 4;
public int InstanceProperty { get; set; } = 5;
public static int StaticMethodReturnsInt()
{
return 2;
}
public static string StaticMethodHasParams(string myValue, Process process)
{
return myValue;
}
public int Method()
{
return 1;
}
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BinaryPack.Attributes;
using KellermanSoftware.CompareNetObjects;
using KellermanSoftware.CompareNetObjects.TypeComparers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test.Helper.Faker;
using NitroxModel_Subnautica.Logger;
using NitroxModel.DataStructures;
namespace NitroxModel.Packets;
[TestClass]
public class PacketsSerializableTest
{
[TestMethod]
public void InitSerializerTest()
{
Packet.InitSerializer();
}
[TestMethod]
public void PacketSerializationTest()
{
ComparisonConfig config = new();
config.SkipInvalidIndexers = true;
config.AttributesToIgnore.Add(typeof(IgnoredMemberAttribute));
config.CustomComparers.Add(new CustomComparer<NitroxId, NitroxId>((id1, id2) => id1.Equals(id2)));
CompareLogic comparer = new(config);
IEnumerable<Type> types = typeof(Packet).Assembly.GetTypes().Concat(typeof(SubnauticaInGameLogger).Assembly.GetTypes());
Type[] packetTypes = types.Where(p => typeof(Packet).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract).ToArray();
// We want to ignore packets with no members when using ShouldNotCompare
Type[] emptyPackets = packetTypes.Where(t => !t.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.Any(member => member.MemberType is MemberTypes.Field or MemberTypes.Property &&
!member.GetCustomAttributes<IgnoredMemberAttribute>().Any()))
.ToArray();
// We generate two different versions of each packet to verify comparison is actually working
List<(Packet, Packet)> generatedPackets = new();
foreach (Type type in packetTypes)
{
dynamic faker = NitroxFaker.GetOrCreateFaker(type);
Packet packet = faker.Generate();
Packet packet2 = null;
if (!emptyPackets.Contains(type))
{
ComparisonResult result;
do
{
packet2 = faker.Generate();
result = comparer.Compare(packet, packet2);
} while (result == null || result.AreEqual);
}
generatedPackets.Add(new ValueTuple<Packet, Packet>(packet, packet2));
}
Packet.InitSerializer();
foreach (ValueTuple<Packet, Packet> packet in generatedPackets)
{
Packet deserialized = Packet.Deserialize(packet.Item1.Serialize());
packet.Item1.ShouldCompare(deserialized, $"with {packet.Item1.GetType()}", config);
if (!emptyPackets.Contains(packet.Item1.GetType()))
{
packet.Item2.ShouldNotCompare(deserialized, $"with {packet.Item1.GetType()}", config);
}
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test;
using NitroxClient;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Core;
using NitroxModel.Packets.Processors.Abstract;
using NitroxServer;
using NitroxServer.Communication.Packets;
using NitroxServer.Communication.Packets.Processors;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer_Subnautica;
namespace NitroxModel.Packets.Processors
{
[TestClass]
public class PacketProcessorTest
{
[TestMethod]
public void ClientPacketProcessorSanity()
{
typeof(ClientPacketProcessor<>).Assembly.GetTypes()
.Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList()
.ForEach(processor =>
{
// Make sure that each packet-processor is derived from the ClientPacketProcessor class,
// so that it's packet-type can be determined.
Assert.IsNotNull(processor.BaseType, $"{processor} does not derive from any type!");
Assert.IsTrue(processor.BaseType.IsGenericType, $"{processor} does not derive from a generic type!");
Assert.IsTrue(processor.BaseType.IsAssignableToGenericType(typeof(ClientPacketProcessor<>)), $"{processor} does not derive from ClientPacketProcessor!");
// Check constructor availability:
int numCtors = processor.GetConstructors().Length;
Assert.IsTrue(numCtors == 1, $"{processor} should have exactly 1 constructor! (has {numCtors})");
});
}
[TestMethod]
public void ServerPacketProcessorSanity()
{
typeof(PacketHandler).Assembly.GetTypes()
.Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList()
.ForEach(processor =>
{
// Make sure that each packet-processor is derived from the ClientPacketProcessor class,
// so that it's packet-type can be determined.
Assert.IsNotNull(processor.BaseType, $"{processor} does not derive from any type!");
Assert.IsTrue(processor.BaseType.IsGenericType, $"{processor} does not derive from a generic type!");
Assert.IsTrue(processor.BaseType.IsAssignableToGenericType(typeof(AuthenticatedPacketProcessor<>)) ||
processor.BaseType.IsAssignableToGenericType(typeof(UnauthenticatedPacketProcessor<>)), $"{processor} does not derive from (Un)AuthenticatedPacketProcessor!");
// Check constructor availability:
int numCtors = processor.GetConstructors().Length;
Assert.IsTrue(numCtors == 1, $"{processor} should have exactly 1 constructor! (has {numCtors})");
// Unable to check parameters, these are defined in PacketHandler.ctor
});
}
[TestMethod]
public void SameAmountOfServerPacketProcessors()
{
IEnumerable<Type> processors = typeof(PacketHandler).Assembly.GetTypes()
.Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract);
ServerAutoFacRegistrar serverDependencyRegistrar = new ServerAutoFacRegistrar();
NitroxServiceLocator.InitializeDependencyContainer(serverDependencyRegistrar);
NitroxServiceLocator.BeginNewLifetimeScope();
List<Type> packetTypes = typeof(DefaultServerPacketProcessor).Assembly.GetTypes()
.Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList();
int both = packetTypes.Count;
Assert.AreEqual(processors.Count(), both,
$"Not all(Un) AuthenticatedPacketProcessors have been discovered by the runtime code (auth + unauth: {both} out of {processors.Count()}). Perhaps the runtime matching code is too strict, or a processor does not derive from ClientPacketProcessor (and will hence not be detected).");
}
[TestMethod]
public void AllPacketsAreHandled()
{
List<Type> packetTypes = typeof(DefaultServerPacketProcessor).Assembly.GetTypes()
.Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
.ToList();
List<Type> abstractProcessorTypes = new();
abstractProcessorTypes.AddRange(typeof(ClientPacketProcessor<>)
.Assembly.GetTypes()
.Where(p => p.IsClass && p.IsAbstract && p.IsAssignableToGenericType(typeof(ClientPacketProcessor<>))));
abstractProcessorTypes.AddRange(typeof(AuthenticatedPacketProcessor<>)
.Assembly.GetTypes()
.Where(p => p.IsClass && p.IsAbstract && (p.IsAssignableToGenericType(typeof(AuthenticatedPacketProcessor<>)) || p.IsAssignableToGenericType(typeof(UnauthenticatedPacketProcessor<>)))));
NitroxServiceLocator.InitializeDependencyContainer(new ClientAutoFacRegistrar(), new SubnauticaServerAutoFacRegistrar(), new TestAutoFacRegistrar());
NitroxServiceLocator.BeginNewLifetimeScope();
foreach (Type packet in typeof(Packet).Assembly.GetTypes().Where(p => typeof(Packet).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract).ToList())
{
Assert.IsTrue(packetTypes.Contains(packet) || abstractProcessorTypes.Any(genericProcessor =>
{
Type processorType = genericProcessor.MakeGenericType(packet);
return NitroxServiceLocator.LocateOptionalService(processorType).HasValue;
}), $"Packet of type '{packet}' should have at least one processor.");
}
}
[TestCleanup]
public void Cleanup()
{
NitroxServiceLocator.EndCurrentLifetimeScope();
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Nitrox.Test.Model.Platforms;
namespace NitroxModel.Platforms.OS.Windows;
[TestClass]
[SupportedOSPlatform("windows")]
public class RegistryTest
{
[OSTestMethod("windows")]
public async Task WaitsForRegistryKeyToExist()
{
const string PATH_TO_KEY = @"SOFTWARE\Nitrox\test";
RegistryEx.Write(PATH_TO_KEY, 0);
Task<bool> readTask = Task.Run(async () =>
{
try
{
await RegistryEx.CompareWaitAsync<int>(PATH_TO_KEY,
v => v == 1337,
TimeSpan.FromSeconds(5));
return true;
}
catch (TaskCanceledException)
{
return false;
}
});
RegistryEx.Write(PATH_TO_KEY, 1337);
Assert.IsTrue(await readTask);
// Cleanup (we can keep "Nitrox" key intact).
RegistryEx.Delete(PATH_TO_KEY);
Assert.IsNull(RegistryEx.Read<string>(PATH_TO_KEY));
}
}

View File

@@ -0,0 +1,32 @@
namespace Nitrox.Test.Model.Platforms;
[AttributeUsage(AttributeTargets.Method)]
public class OSTestMethodAttribute : TestMethodAttribute
{
public string Platform { get;}
/// <summary>
/// Test method attribute, that will only run the test on the specified platform.
/// </summary>
/// <param name="platform">case insensitive platform, i.e: linux, windows, osx</param>
public OSTestMethodAttribute(string platform)
{
Platform = platform;
}
public override TestResult[] Execute(ITestMethod testMethod)
{
if (!OperatingSystem.IsOSPlatform(Platform))
{
return [
new TestResult()
{
Outcome = UnitTestOutcome.Inconclusive,
TestContextMessages = $"This test can only be run on {Platform}"
}
];
}
return base.Execute(testMethod);
}
}

View File

@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<!-- TODO: Do not use DI in tests, create objects in-line or use mocks for externalities -->
<NoWarn>$(NoWarn);DIMA001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="CompareNETObjects" Version="4.83.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="[7.2.0]" />
<PackageReference Include="FluentAssertions.Analyzers" Version="[0.34.1]">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MSTest" Version="3.8.3" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NitroxClient\NitroxClient.csproj" />
<ProjectReference Include="..\NitroxModel\NitroxModel.csproj" />
<ProjectReference Include="..\NitroxPatcher\NitroxPatcher.csproj" />
<ProjectReference Include="..\NitroxServer-Subnautica\NitroxServer-Subnautica.csproj" />
<ProjectReference Include="..\NitroxServer\NitroxServer.csproj" />
</ItemGroup>
<Target Name="MoveNitroxAssetsToTestOutput" AfterTargets="Build">
<ItemGroup>
<NitroxSubnauticaAssets Include="..\Nitrox.Assets.Subnautica\**\*." />
<NitroxSubnauticaStaticDlls Include="..\Nitrox.Assets.Subnautica\**\*.dll" />
<NitroxSubnauticaResources Include="..\Nitrox.Assets.Subnautica\Resources\*.*" />
<NitroxSubnauticaLanguageFiles Include="..\Nitrox.Assets.Subnautica\LanguageFiles\*.json" />
</ItemGroup>
<Copy SourceFiles="@(NitroxSubnauticaAssets)" DestinationFolder="$(TargetDir)\%(RecursiveDir)" />
<Copy SourceFiles="@(NitroxSubnauticaStaticDlls)" DestinationFolder="$(TargetDir)\lib\%(RecursiveDir)" />
<Copy SourceFiles="@(NitroxSubnauticaLanguageFiles)" DestinationFolder="$(TargetDir)\LanguageFiles\%(RecursiveDir)" />
<Copy SourceFiles="@(NitroxSubnauticaResources)" DestinationFolder="$(TargetDir)\Resources\%(RecursiveDir)" />
</Target>
</Project>

View File

@@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using FluentAssertions;
using HarmonyLib;
using NitroxModel.Helper;
using NitroxPatcher.PatternMatching;
namespace NitroxTest.Patcher
{
public static class PatchTestHelper
{
public static List<CodeInstruction> GenerateDummyInstructions(int count)
{
List<CodeInstruction> instructions = new List<CodeInstruction>();
for (int i = 0; i < count; i++)
{
instructions.Add(new CodeInstruction(OpCodes.Nop));
}
return instructions;
}
public static ReadOnlyCollection<CodeInstruction> GetInstructionsFromMethod(DynamicMethod targetMethod)
{
Validate.NotNull(targetMethod);
return GetInstructionsFromIL(GetILInstructions(targetMethod));
}
public static ReadOnlyCollection<CodeInstruction> GetInstructionsFromMethod(MethodInfo targetMethod)
{
Validate.NotNull(targetMethod);
return GetInstructionsFromIL(GetILInstructions(targetMethod));
}
public static IEnumerable<KeyValuePair<OpCode, object>> GetILInstructions(MethodInfo method)
{
return PatchProcessor.ReadMethodBody(method, method.GetILGenerator());
}
public static IEnumerable<KeyValuePair<OpCode, object>> GetILInstructions(DynamicMethod method)
{
return PatchProcessor.ReadMethodBody(method, method.GetILGenerator());
}
public static ILGenerator GetILGenerator(this MethodInfo method)
{
return new DynamicMethod(method.Name, method.ReturnType, method.GetParameters().Types()).GetILGenerator();
}
public static void TestPattern(MethodInfo targetMethod, InstructionsPattern pattern, out IEnumerable<CodeInstruction> originalIl, out IEnumerable<CodeInstruction> transformedIl)
{
bool shouldHappen = false;
originalIl = PatchProcessor.GetCurrentInstructions(targetMethod);
transformedIl = originalIl
.Transform(pattern, (_, _) =>
{
shouldHappen = true;
})
.ToArray(); // Required, otherwise nothing happens.
shouldHappen.Should().BeTrue();
}
/// <summary>
/// Clones the instructions so that the returned instructions are not the same reference.
/// </summary>
/// <remarks>
/// Useful for testing code differences before and after a Harmony transpiler.
/// </remarks>
public static List<CodeInstruction> Clone(this IEnumerable<CodeInstruction> instructions)
{
return new List<CodeInstruction>(instructions.Select(il => new CodeInstruction(il)));
}
private static ReadOnlyCollection<CodeInstruction> GetInstructionsFromIL(IEnumerable<KeyValuePair<OpCode, object>> il)
{
List<CodeInstruction> result = new List<CodeInstruction>();
foreach (KeyValuePair<OpCode, object> instruction in il)
{
result.Add(new CodeInstruction(instruction.Key, instruction.Value));
}
return result.AsReadOnly();
}
}
}

View File

@@ -0,0 +1,218 @@
using System.Reflection.Emit;
using HarmonyLib;
using NitroxPatcher.Patches;
using NitroxPatcher.Patches.Dynamic;
using NitroxPatcher.Patches.Persistent;
using NitroxPatcher.PatternMatching;
using NitroxTest.Patcher;
namespace Nitrox.Test.Patcher.Patches;
[TestClass]
public class PatchesTranspilerTest
{
// Add "true" to any of those elements to have its transformed IL printed.
public static IEnumerable<object[]> TranspilerPatchClasses =>
[
[typeof(AggressiveWhenSeeTarget_ScanForAggressionTarget_Patch), 3],
[typeof(AttackCyclops_OnCollisionEnter_Patch), -17],
[typeof(AttackCyclops_UpdateAggression_Patch), -23],
[typeof(Bullet_Update_Patch), 3],
[typeof(BaseDeconstructable_Deconstruct_Patch), BaseDeconstructable_Deconstruct_Patch.InstructionsToAdd(true).Count() * 2],
[typeof(BaseHullStrength_CrushDamageUpdate_Patch), 3],
[typeof(BreakableResource_SpawnResourceFromPrefab_Patch), 2],
[typeof(Builder_TryPlace_Patch), Builder_TryPlace_Patch.InstructionsToAdd1.Count + Builder_TryPlace_Patch.InstructionsToAdd2.Count],
[typeof(CellManager_TryLoadCacheBatchCells_Patch), 4],
[typeof(Constructable_Construct_Patch), Constructable_Construct_Patch.InstructionsToAdd.Count],
[typeof(Constructable_DeconstructAsync_Patch), Constructable_DeconstructAsync_Patch.InstructionsToAdd.Count],
[typeof(ConstructableBase_SetState_Patch), ConstructableBase_SetState_Patch.InstructionsToAdd.Count],
[typeof(ConstructorInput_OnCraftingBegin_Patch), 7],
[typeof(CrafterLogic_TryPickupSingleAsync_Patch), 4],
[typeof(CrashHome_Spawn_Patch), 2],
[typeof(CrashHome_Update_Patch), -5],
[typeof(CreatureDeath_OnKillAsync_Patch), 9],
[typeof(CreatureDeath_SpawnRespawner_Patch), 2],
[typeof(CyclopsDestructionEvent_DestroyCyclops_Patch), 3],
[typeof(CyclopsDestructionEvent_SpawnLootAsync_Patch), 7],
[typeof(CyclopsShieldButton_OnClick_Patch), -6],
[typeof(CyclopsSonarButton_Update_Patch), 3],
[typeof(CyclopsSonarDisplay_NewEntityOnSonar_Patch), 3],
[typeof(DevConsole_Update_Patch), 0],
[typeof(Eatable_IterateDespawn_Patch), 2],
[typeof(EnergyMixin_SpawnDefaultAsync_Patch), -64],
[typeof(EntityCell_AwakeAsync_Patch), 2],
[typeof(EntityCell_SleepAsync_Patch), 2],
[typeof(Equipment_RemoveItem_Patch), 7],
[typeof(EscapePod_Start_Patch), 43],
[typeof(FireExtinguisherHolder_TakeTankAsync_Patch), 2],
[typeof(FireExtinguisherHolder_TryStoreTank_Patch), 3],
[typeof(Flare_Update_Patch), 0],
[typeof(FootstepSounds_OnStep_Patch), 6],
[typeof(GameInput_Initialize_Patch), 5],
[typeof(GrowingPlant_SpawnGrownModelAsync_Patch), -1],
[typeof(Player_TriggerInfectionRevealAsync_Patch), 1],
[typeof(IngameMenu_OnSelect_Patch), -2],
[typeof(IngameMenu_QuitGameAsync_Patch), 2],
[typeof(IngameMenu_QuitSubscreen_Patch), -24],
[typeof(ItemsContainer_DestroyItem_Patch), 2],
[typeof(Knife_OnToolUseAnim_Patch), 0],
[typeof(LargeRoomWaterPark_OnDeconstructionStart_Patch), 3],
[typeof(LargeWorldEntity_UpdateCell_Patch), 1],
[typeof(LaunchRocket_OnHandClick_Patch), -9],
[typeof(LeakingRadiation_Update_Patch), 0],
[typeof(MainGameController_StartGame_Patch), 1],
[typeof(MeleeAttack_CanDealDamageTo_Patch), 4],
[typeof(PDAScanner_Scan_Patch), 3],
[typeof(PickPrefab_AddToContainerAsync_Patch), 4],
[typeof(Player_OnKill_Patch), 0],
[typeof(Respawn_Start_Patch), 3],
[typeof(RocketConstructor_StartRocketConstruction_Patch), 3],
[typeof(SpawnConsoleCommand_SpawnAsync_Patch), 2],
[typeof(SpawnOnKill_OnKill_Patch), 3],
[typeof(SubConsoleCommand_OnConsoleCommand_sub_Patch), 0],
[typeof(SubRoot_OnPlayerEntered_Patch), 5],
[typeof(Trashcan_Update_Patch), 4],
[typeof(uGUI_OptionsPanel_AddAccessibilityTab_Patch), -10],
[typeof(uGUI_PDA_Initialize_Patch), 2],
[typeof(uGUI_PDA_SetTabs_Patch), 3],
[typeof(uGUI_Pings_IsVisibleNow_Patch), 0],
[typeof(uGUI_SceneIntro_HandleInput_Patch), -2],
[typeof(uGUI_SceneIntro_IntroSequence_Patch), 8],
[typeof(uSkyManager_SetVaryingMaterialProperties_Patch), 0],
[typeof(Welder_Weld_Patch), 1],
[typeof(Poop_Perform_Patch), 1],
[typeof(SeaDragonMeleeAttack_OnTouchFront_Patch), 9],
[typeof(SeaDragonMeleeAttack_SwatAttack_Patch), 4],
[typeof(SeaTreaderSounds_SpawnChunks_Patch), 3],
[typeof(Vehicle_TorpedoShot_Patch), 3],
[typeof(SeamothTorpedo_Update_Patch), 0],
[typeof(SeaTreader_UpdatePath_Patch), 0],
[typeof(SeaTreader_UpdateTurning_Patch), 0],
[typeof(SeaTreader_Update_Patch), 0],
[typeof(StasisSphere_LateUpdate_Patch), 0],
[typeof(WaterParkCreature_BornAsync_Patch), 6],
[typeof(WaterParkCreature_ManagedUpdate_Patch), 2],
];
[TestMethod]
public void AllTranspilerPatchesHaveSanityTest()
{
Type[] allPatchesWithTranspiler = typeof(NitroxPatcher.Main).Assembly.GetTypes().Where(p => typeof(NitroxPatch).IsAssignableFrom(p) && p.IsClass).Where(x => x.GetMethod("Transpiler") != null).ToArray();
foreach (Type patch in allPatchesWithTranspiler)
{
if (TranspilerPatchClasses.All(x => (Type)x[0] != patch))
{
Assert.Fail($"{patch.Name} has an Transpiler but is not included in the test-suit and {nameof(TranspilerPatchClasses)}.");
}
}
}
[TestMethod]
[DynamicData(nameof(TranspilerPatchClasses))]
public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bool logInstructions = false)
{
FieldInfo targetMethodInfo = patchClassType.GetRuntimeFields().FirstOrDefault(x => string.Equals(x.Name.Replace("_", ""), "targetMethod", StringComparison.OrdinalIgnoreCase));
if (targetMethodInfo == null)
{
Assert.Fail($"Could not find either \"TARGET_METHOD\" nor \"targetMethod\" inside {patchClassType.Name}");
}
MethodInfo targetMethod = targetMethodInfo.GetValue(null) as MethodInfo;
List<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList();
List<CodeInstruction> originalIlCopy = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList(); // Our custom pattern matching replaces OpCode/Operand in place, therefor we need a copy to compare if changes are present
MethodInfo transpilerMethod = patchClassType.GetMethod("Transpiler");
if (transpilerMethod == null)
{
Assert.Fail($"Could not find \"Transpiler\" inside {patchClassType.Name}");
}
List<object> injectionParameters = [];
foreach (ParameterInfo parameterInfo in transpilerMethod.GetParameters())
{
if (parameterInfo.ParameterType == typeof(MethodBase))
{
injectionParameters.Add(targetMethod);
}
else if (parameterInfo.ParameterType == typeof(IEnumerable<CodeInstruction>))
{
injectionParameters.Add(originalIl);
}
else if (parameterInfo.ParameterType == typeof(ILGenerator))
{
injectionParameters.Add(GetILGenerator(targetMethod, patchClassType));
}
else
{
Assert.Fail($"Unexpected parameter type: {parameterInfo.ParameterType} inside Transpiler method of {patchClassType.Name}");
}
}
List<CodeInstruction> transformedIl = (transpilerMethod.Invoke(null, injectionParameters.ToArray()) as IEnumerable<CodeInstruction>)?.ToList();
if (logInstructions)
{
Console.WriteLine(transformedIl.ToPrettyString());
}
if (transformedIl == null || transformedIl.Count == 0)
{
Assert.Fail($"Calling {patchClassType.Name}.Transpiler() through reflection returned null or an empty list.");
}
originalIlCopy.Count.Should().Be(transformedIl.Count - ilDifference);
Assert.IsFalse(originalIlCopy.SequenceEqual(transformedIl, new CodeInstructionComparer()), $"The transpiler patch of {patchClassType.Name} did not change the IL");
}
private static readonly ModuleBuilder patchTestModule;
static PatchesTranspilerTest()
{
AssemblyName asmName = new();
asmName.Name = "PatchTestAssembly";
PersistedAssemblyBuilder myAsmBuilder = new(asmName, typeof(object).Assembly);
patchTestModule = myAsmBuilder.DefineDynamicModule(asmName.Name);
}
/// This complicated generation is required for ILGenerator.DeclareLocal to work
private static ILGenerator GetILGenerator(MethodInfo method, Type generatingType)
{
TypeBuilder myTypeBld = patchTestModule.DefineType($"{generatingType}_PatchTestType", TypeAttributes.Public);
return myTypeBld.DefineMethod(method.Name, MethodAttributes.Public, method.ReturnType, method.GetParameters().Types()).GetILGenerator();
}
}
public class CodeInstructionComparer : IEqualityComparer<CodeInstruction>
{
public bool Equals(CodeInstruction x, CodeInstruction y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x is null)
{
return false;
}
if (y is null)
{
return false;
}
if (x.GetType() != y.GetType())
{
return false;
}
return x.opcode.Equals(y.opcode) && Equals(x.operand, y.operand);
}
public int GetHashCode(CodeInstruction obj)
{
unchecked
{
return (obj.opcode.GetHashCode() * 397) ^ (obj.operand != null ? obj.operand.GetHashCode() : 0);
}
}
}

View File

@@ -0,0 +1,29 @@
using LitJson;
using NitroxModel.Helper;
namespace Nitrox.Test.Patcher.Patches.Persistent;
[TestClass]
public class Language_LoadLanguageFile_PatchTest
{
[TestMethod]
public void DefaultLanguageSanity()
{
string languageFolder = Path.Combine(NitroxUser.AssetsPath, "LanguageFiles");
Assert.IsTrue(Directory.Exists(languageFolder), $"The language files folder does not exist at {languageFolder}.");
string defaultLanguageFilePath = Path.Combine(languageFolder, "en.json");
Assert.IsTrue(File.Exists(defaultLanguageFilePath), $"The english language file does not exist at {defaultLanguageFilePath}.");
using StreamReader streamReader = new(defaultLanguageFilePath);
try
{
JsonData defaultLanguage = JsonMapper.ToObject(streamReader);
Assert.IsTrue(defaultLanguage.Keys.Count > 0, "The english language file has no entries.");
}
catch (Exception ex)
{
Assert.Fail($"Unable to map default json file : {ex.Message}");
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxServer.Helper;
namespace Nitrox.Test.Server.Helper;
[TestClass]
public class XORRandomTest
{
[TestMethod]
public void TestMeanGeneration()
{
// arbitrary values under there but we can't compare the generated values with UnityEngine.Random because it's unaccessible
XORRandom.InitSeed("cheescake".GetHashCode());
float mean = 0;
int count = 1000000;
for (int i = 0; i < count; i++)
{
mean += XORRandom.NextFloat();
}
mean /= count;
Assert.IsTrue(Math.Abs(0.5f - mean) < 0.001f, $"Float number generation isn't uniform enough: {mean}");
}
}

View File

@@ -0,0 +1,508 @@
using Nitrox.Test;
using Nitrox.Test.Helper.Faker;
using NitroxModel.Core;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata.Bases;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
using NitroxServer.Serialization.World;
using NitroxServer_Subnautica;
namespace NitroxServer.Serialization;
[TestClass]
public class WorldPersistenceTest
{
private static readonly string tempSaveFilePath = Path.Combine(Path.GetTempPath(), "NitroxTestTempDir");
private static PersistedWorldData worldData;
public static PersistedWorldData[] WorldsDataAfter { get; private set; }
public static IServerSerializer[] ServerSerializers { get; private set; }
[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
NitroxServiceLocator.InitializeDependencyContainer(new SubnauticaServerAutoFacRegistrar(), new TestAutoFacRegistrar());
NitroxServiceLocator.BeginNewLifetimeScope();
WorldPersistence worldPersistence = NitroxServiceLocator.LocateService<WorldPersistence>();
ServerSerializers = NitroxServiceLocator.LocateService<IServerSerializer[]>();
WorldsDataAfter = new PersistedWorldData[ServerSerializers.Length];
worldData = new NitroxAutoFaker<PersistedWorldData>().Generate();
for (int index = 0; index < ServerSerializers.Length; index++)
{
//Checking saving
worldPersistence.UpdateSerializer(ServerSerializers[index]);
Assert.IsTrue(worldPersistence.Save(worldData, tempSaveFilePath), $"Saving normal world failed while using {ServerSerializers[index]}.");
//Checking loading
WorldsDataAfter[index] = worldPersistence.LoadDataFromPath(tempSaveFilePath);
Assert.IsNotNull(WorldsDataAfter[index], $"Loading saved world failed while using {ServerSerializers[index]}.");
}
}
[DataTestMethod, DynamicWorldDataAfter]
public void WorldDataTest(PersistedWorldData worldDataAfter, string serializerName)
{
Assert.IsTrue(worldData.WorldData.ParsedBatchCells.SequenceEqual(worldDataAfter.WorldData.ParsedBatchCells));
Assert.AreEqual(worldData.WorldData.Seed, worldDataAfter.WorldData.Seed);
PDAStateTest(worldData.WorldData.GameData.PDAState, worldDataAfter.WorldData.GameData.PDAState);
StoryGoalTest(worldData.WorldData.GameData.StoryGoals, worldDataAfter.WorldData.GameData.StoryGoals);
StoryTimingTest(worldData.WorldData.GameData.StoryTiming, worldDataAfter.WorldData.GameData.StoryTiming);
}
private static void PDAStateTest(PDAStateData pdaState, PDAStateData pdaStateAfter)
{
Assert.IsTrue(pdaState.KnownTechTypes.SequenceEqual(pdaStateAfter.KnownTechTypes));
Assert.IsTrue(pdaState.AnalyzedTechTypes.SequenceEqual(pdaStateAfter.AnalyzedTechTypes));
AssertHelper.IsListEqual(pdaState.PdaLog.OrderBy(x => x.Key), pdaStateAfter.PdaLog.OrderBy(x => x.Key), (entry, entryAfter) =>
{
Assert.AreEqual(entry.Key, entryAfter.Key);
Assert.AreEqual(entry.Timestamp, entryAfter.Timestamp);
});
Assert.IsTrue(pdaState.EncyclopediaEntries.SequenceEqual(pdaStateAfter.EncyclopediaEntries));
Assert.IsTrue(pdaState.ScannerFragments.SequenceEqual(pdaStateAfter.ScannerFragments));
AssertHelper.IsListEqual(pdaState.ScannerPartial.OrderBy(x => x.TechType.Name), pdaStateAfter.ScannerPartial.OrderBy(x => x.TechType.Name), (entry, entryAfter) =>
{
Assert.AreEqual(entry.TechType, entryAfter.TechType);
Assert.AreEqual(entry.Unlocked, entryAfter.Unlocked);
});
Assert.IsTrue(pdaState.ScannerComplete.SequenceEqual(pdaStateAfter.ScannerComplete));
}
private static void StoryGoalTest(StoryGoalData storyGoal, StoryGoalData storyGoalAfter)
{
Assert.IsTrue(storyGoal.CompletedGoals.SequenceEqual(storyGoalAfter.CompletedGoals));
Assert.IsTrue(storyGoal.RadioQueue.SequenceEqual(storyGoalAfter.RadioQueue));
AssertHelper.IsListEqual(storyGoal.ScheduledGoals.OrderBy(x => x.GoalKey), storyGoalAfter.ScheduledGoals.OrderBy(x => x.GoalKey), (scheduledGoal, scheduledGoalAfter) =>
{
Assert.AreEqual(scheduledGoal.TimeExecute, scheduledGoalAfter.TimeExecute);
Assert.AreEqual(scheduledGoal.GoalKey, scheduledGoalAfter.GoalKey);
Assert.AreEqual(scheduledGoal.GoalType, scheduledGoalAfter.GoalType);
});
}
private static void StoryTimingTest(StoryTimingData storyTiming, StoryTimingData storyTimingAfter)
{
Assert.AreEqual(storyTiming.ElapsedSeconds, storyTimingAfter.ElapsedSeconds);
Assert.AreEqual(storyTiming.AuroraCountdownTime, storyTimingAfter.AuroraCountdownTime);
Assert.AreEqual(storyTiming.AuroraWarningTime, storyTimingAfter.AuroraWarningTime);
}
[DataTestMethod, DynamicWorldDataAfter]
public void PlayerDataTest(PersistedWorldData worldDataAfter, string serializerName)
{
AssertHelper.IsListEqual(worldData.PlayerData.Players.OrderBy(x => x.Id), worldDataAfter.PlayerData.Players.OrderBy(x => x.Id), (playerData, playerDataAfter) =>
{
Assert.AreEqual(playerData.Name, playerDataAfter.Name);
Assert.IsTrue(playerData.UsedItems.SequenceEqual(playerDataAfter.UsedItems));
Assert.IsTrue(playerData.QuickSlotsBindingIds.SequenceEqual(playerDataAfter.QuickSlotsBindingIds));
AssertHelper.IsDictionaryEqual(playerData.EquippedItems, playerDataAfter.EquippedItems, (keyValuePair, keyValuePairAfter) =>
{
Assert.AreEqual(keyValuePair.Key, keyValuePairAfter.Key);
Assert.AreEqual(keyValuePair.Value, keyValuePairAfter.Value);
});
Assert.AreEqual(playerData.Id, playerDataAfter.Id);
Assert.AreEqual(playerData.SpawnPosition, playerDataAfter.SpawnPosition);
Assert.AreEqual(playerData.SpawnRotation, playerDataAfter.SpawnRotation);
Assert.AreEqual(playerData.CurrentStats.Oxygen, playerDataAfter.CurrentStats.Oxygen);
Assert.AreEqual(playerData.CurrentStats.MaxOxygen, playerDataAfter.CurrentStats.MaxOxygen);
Assert.AreEqual(playerData.CurrentStats.Health, playerDataAfter.CurrentStats.Health);
Assert.AreEqual(playerData.CurrentStats.Food, playerDataAfter.CurrentStats.Food);
Assert.AreEqual(playerData.CurrentStats.Water, playerDataAfter.CurrentStats.Water);
Assert.AreEqual(playerData.CurrentStats.InfectionAmount, playerDataAfter.CurrentStats.InfectionAmount);
Assert.AreEqual(playerData.SubRootId, playerDataAfter.SubRootId);
Assert.AreEqual(playerData.Permissions, playerDataAfter.Permissions);
Assert.AreEqual(playerData.NitroxId, playerDataAfter.NitroxId);
Assert.AreEqual(playerData.IsPermaDeath, playerDataAfter.IsPermaDeath);
Assert.IsTrue(playerData.PersonalCompletedGoalsWithTimestamp.SequenceEqual(playerDataAfter.PersonalCompletedGoalsWithTimestamp));
AssertHelper.IsDictionaryEqual(playerData.PlayerPreferences.PingPreferences, playerDataAfter.PlayerPreferences.PingPreferences, (keyValuePair, keyValuePairAfter) =>
{
Assert.AreEqual(keyValuePair.Key, keyValuePairAfter.Key);
Assert.AreEqual(keyValuePair.Value.Color, keyValuePairAfter.Value.Color);
Assert.AreEqual(keyValuePair.Value.Visible, keyValuePairAfter.Value.Visible);
});
Assert.IsTrue(playerData.PlayerPreferences.PinnedTechTypes.SequenceEqual(playerDataAfter.PlayerPreferences.PinnedTechTypes));
});
}
[DataTestMethod, DynamicWorldDataAfter]
public void EntityDataTest(PersistedWorldData worldDataAfter, string serializerName)
{
AssertHelper.IsListEqual(worldData.EntityData.Entities.OrderBy(x => x.Id), worldDataAfter.EntityData.Entities.OrderBy(x => x.Id), EntityTest);
}
[DataTestMethod, DynamicWorldDataAfter]
public void GlobalRootDataTest(PersistedWorldData worldDataAfter, string serializerName)
{
AssertHelper.IsListEqual(worldData.GlobalRootData.Entities.OrderBy(x => x.Id), worldDataAfter.GlobalRootData.Entities.OrderBy(x => x.Id), EntityTest);
}
private static void EntityTest(Entity entity, Entity entityAfter)
{
Assert.AreEqual(entity.Id, entityAfter.Id);
Assert.AreEqual(entity.TechType, entityAfter.TechType);
Assert.AreEqual(entity.ParentId, entityAfter.ParentId);
switch (entity.Metadata)
{
case KeypadMetadata metadata when entityAfter.Metadata is KeypadMetadata metadataAfter:
Assert.AreEqual(metadata.Unlocked, metadataAfter.Unlocked);
break;
case SealedDoorMetadata metadata when entityAfter.Metadata is SealedDoorMetadata metadataAfter:
Assert.AreEqual(metadata.Sealed, metadataAfter.Sealed);
Assert.AreEqual(metadata.OpenedAmount, metadataAfter.OpenedAmount);
break;
case PrecursorDoorwayMetadata metadata when entityAfter.Metadata is PrecursorDoorwayMetadata metadataAfter:
Assert.AreEqual(metadata.IsOpen, metadataAfter.IsOpen);
break;
case PrecursorTeleporterMetadata metadata when entityAfter.Metadata is PrecursorTeleporterMetadata metadataAfter:
Assert.AreEqual(metadata.IsOpen, metadataAfter.IsOpen);
break;
case PrecursorKeyTerminalMetadata metadata when entityAfter.Metadata is PrecursorKeyTerminalMetadata metadataAfter:
Assert.AreEqual(metadata.Slotted, metadataAfter.Slotted);
break;
case PrecursorTeleporterActivationTerminalMetadata metadata when entityAfter.Metadata is PrecursorTeleporterActivationTerminalMetadata metadataAfter:
Assert.AreEqual(metadata.Unlocked, metadataAfter.Unlocked);
break;
case StarshipDoorMetadata metadata when entityAfter.Metadata is StarshipDoorMetadata metadataAfter:
Assert.AreEqual(metadata.DoorLocked, metadataAfter.DoorLocked);
Assert.AreEqual(metadata.DoorOpen, metadataAfter.DoorOpen);
break;
case WeldableWallPanelGenericMetadata metadata when entityAfter.Metadata is WeldableWallPanelGenericMetadata metadataAfter:
Assert.AreEqual(metadata.LiveMixInHealth, metadataAfter.LiveMixInHealth);
break;
case IncubatorMetadata metadata when entityAfter.Metadata is IncubatorMetadata metadataAfter:
Assert.AreEqual(metadata.Powered, metadataAfter.Powered);
Assert.AreEqual(metadata.Hatched, metadataAfter.Hatched);
break;
case EntitySignMetadata metadata when entityAfter.Metadata is EntitySignMetadata metadataAfter:
Assert.AreEqual(metadata.Text, metadataAfter.Text);
Assert.AreEqual(metadata.ColorIndex, metadataAfter.ColorIndex);
Assert.AreEqual(metadata.ScaleIndex, metadataAfter.ScaleIndex);
Assert.IsTrue(metadata.Elements.SequenceEqual(metadataAfter.Elements));
Assert.AreEqual(metadata.Background, metadataAfter.Background);
break;
case ConstructorMetadata metadata when entityAfter.Metadata is ConstructorMetadata metadataAfter:
Assert.AreEqual(metadata.Deployed, metadataAfter.Deployed);
break;
case FlashlightMetadata metadata when entityAfter.Metadata is FlashlightMetadata metadataAfter:
Assert.AreEqual(metadata.On, metadataAfter.On);
break;
case BatteryMetadata metadata when entityAfter.Metadata is BatteryMetadata metadataAfter:
Assert.AreEqual(metadata.Charge, metadataAfter.Charge);
break;
case EscapePodMetadata metadata when entityAfter.Metadata is EscapePodMetadata metadataAfter:
Assert.AreEqual(metadata.PodRepaired, metadataAfter.PodRepaired);
Assert.AreEqual(metadata.RadioRepaired, metadataAfter.RadioRepaired);
break;
case CrafterMetadata metadata when entityAfter.Metadata is CrafterMetadata metadataAfter:
Assert.AreEqual(metadata.TechType, metadataAfter.TechType);
Assert.AreEqual(metadata.StartTime, metadataAfter.StartTime);
Assert.AreEqual(metadata.Duration, metadataAfter.Duration);
break;
case PlantableMetadata metadata when entityAfter.Metadata is PlantableMetadata metadataAfter:
Assert.AreEqual(metadata.TimeStartGrowth, metadataAfter.TimeStartGrowth);
Assert.AreEqual(metadata.SlotID, metadataAfter.SlotID);
// FruitPlantMetadata field is not checked before it's only temporary
break;
case FruitPlantMetadata metadata when entityAfter.Metadata is FruitPlantMetadata metadataAfter:
Assert.IsTrue(metadata.PickedStates.SequenceEqual(metadataAfter.PickedStates));
Assert.AreEqual(metadata.TimeNextFruit, metadataAfter.TimeNextFruit);
break;
case CyclopsMetadata metadata when entityAfter.Metadata is CyclopsMetadata metadataAfter:
Assert.AreEqual(metadata.SilentRunningOn, metadataAfter.SilentRunningOn);
Assert.AreEqual(metadata.ShieldOn, metadataAfter.ShieldOn);
Assert.AreEqual(metadata.SonarOn, metadataAfter.SonarOn);
Assert.AreEqual(metadata.EngineOn, metadataAfter.EngineOn);
Assert.AreEqual(metadata.EngineMode, metadataAfter.EngineMode);
Assert.AreEqual(metadata.Health, metadataAfter.Health);
break;
case SeamothMetadata metadata when entityAfter.Metadata is SeamothMetadata metadataAfter:
Assert.AreEqual(metadata.LightsOn, metadataAfter.LightsOn);
Assert.AreEqual(metadata.Health, metadataAfter.Health);
Assert.AreEqual(metadata.Name, metadataAfter.Name);
Assert.IsTrue(metadata.Colors.SequenceEqual(metadataAfter.Colors));
break;
case ExosuitMetadata metadata when entityAfter.Metadata is ExosuitMetadata metadataAfter:
Assert.AreEqual(metadata.Health, metadataAfter.Health);
Assert.AreEqual(metadata.Name, metadataAfter.Name);
Assert.IsTrue(metadata.Colors.SequenceEqual(metadataAfter.Colors));
break;
case SubNameInputMetadata metadata when entityAfter.Metadata is SubNameInputMetadata metadataAfter:
Assert.AreEqual(metadata.Name, metadataAfter.Name);
Assert.IsTrue(metadata.Colors.SequenceEqual(metadataAfter.Colors));
break;
case RocketMetadata metadata when entityAfter.Metadata is RocketMetadata metadataAfter:
Assert.AreEqual(metadata.CurrentStage, metadataAfter.CurrentStage);
Assert.AreEqual(metadata.LastStageTransitionTime, metadataAfter.LastStageTransitionTime);
Assert.AreEqual(metadata.ElevatorState, metadataAfter.ElevatorState);
Assert.AreEqual(metadata.ElevatorPosition, metadataAfter.ElevatorPosition);
Assert.IsTrue(metadata.PreflightChecks.SequenceEqual(metadataAfter.PreflightChecks));
break;
case CyclopsLightingMetadata metadata when entityAfter.Metadata is CyclopsLightingMetadata metadataAfter:
Assert.AreEqual(metadata.FloodLightsOn, metadataAfter.FloodLightsOn);
Assert.AreEqual(metadata.InternalLightsOn, metadataAfter.InternalLightsOn);
break;
case FireExtinguisherHolderMetadata metadata when entityAfter.Metadata is FireExtinguisherHolderMetadata metadataAfter:
Assert.AreEqual(metadata.HasExtinguisher, metadataAfter.HasExtinguisher);
Assert.AreEqual(metadata.Fuel, metadataAfter.Fuel);
break;
case PlayerMetadata metadata when entityAfter.Metadata is PlayerMetadata metadataAfter:
AssertHelper.IsListEqual(metadata.EquippedItems.OrderBy(x => x.Id), metadataAfter.EquippedItems.OrderBy(x => x.Id), (equippedItem, equippedItemAfter) =>
{
Assert.AreEqual(equippedItem.Id, equippedItemAfter.Id);
Assert.AreEqual(equippedItem.Slot, equippedItemAfter.Slot);
Assert.AreEqual(equippedItem.TechType, equippedItemAfter.TechType);
});
break;
case GhostMetadata ghostMetadata when entityAfter.Metadata is GhostMetadata ghostMetadataAfter:
Assert.AreEqual(ghostMetadata.TargetOffset, ghostMetadataAfter.TargetOffset);
if (ghostMetadata.GetType() != ghostMetadataAfter.GetType())
{
Assert.Fail($"Runtime type of {nameof(GhostMetadata)} in {nameof(Entity)}.{nameof(Entity.Metadata)} is not equal: {ghostMetadata.GetType().Name} - {ghostMetadataAfter.GetType().Name}");
}
switch (ghostMetadata)
{
case BaseAnchoredCellGhostMetadata metadata when ghostMetadataAfter is BaseAnchoredCellGhostMetadata metadataAfter:
Assert.AreEqual(metadata.AnchoredCell, metadataAfter.AnchoredCell);
break;
case BaseAnchoredFaceGhostMetadata metadata when ghostMetadataAfter is BaseAnchoredFaceGhostMetadata metadataAfter:
Assert.AreEqual(metadata.AnchoredFace, metadataAfter.AnchoredFace);
break;
case BaseDeconstructableGhostMetadata metadata when ghostMetadataAfter is BaseDeconstructableGhostMetadata metadataAfter:
Assert.AreEqual(metadata.ModuleFace, metadataAfter.ModuleFace);
Assert.AreEqual(metadata.ClassId, metadataAfter.ClassId);
break;
}
break;
case WaterParkCreatureMetadata metadata when entityAfter.Metadata is WaterParkCreatureMetadata metadataAfter:
Assert.AreEqual(metadata.Age, metadataAfter.Age);
Assert.AreEqual(metadata.MatureTime, metadataAfter.MatureTime);
Assert.AreEqual(metadata.TimeNextBreed, metadataAfter.TimeNextBreed);
Assert.AreEqual(metadata.BornInside, metadataAfter.BornInside);
break;
case FlareMetadata metadata when entityAfter.Metadata is FlareMetadata metadataAfter:
Assert.AreEqual(metadata.EnergyLeft, metadataAfter.EnergyLeft);
Assert.AreEqual(metadata.HasBeenThrown, metadataAfter.HasBeenThrown);
Assert.AreEqual(metadata.FlareActivateTime, metadataAfter.FlareActivateTime);
break;
case BeaconMetadata metadata when entityAfter.Metadata is BeaconMetadata metadataAfter:
Assert.AreEqual(metadata.Label, metadataAfter.Label);
break;
case RadiationMetadata metadata when entityAfter.Metadata is RadiationMetadata metadataAfter:
Assert.AreEqual(metadata.Health, metadataAfter.Health);
Assert.AreEqual(metadata.FixRealTime, metadataAfter.FixRealTime);
break;
case CrashHomeMetadata metadata when entityAfter.Metadata is CrashHomeMetadata metadataAfter:
Assert.AreEqual(metadata.SpawnTime, metadataAfter.SpawnTime);
break;
case EatableMetadata metadata when entityAfter.Metadata is EatableMetadata metadataAfter:
Assert.AreEqual(metadata.TimeDecayStart, metadataAfter.TimeDecayStart);
break;
case SeaTreaderMetadata metadata when entityAfter.Metadata is SeaTreaderMetadata metadataAfter:
Assert.AreEqual(metadata.ReverseDirection, metadataAfter.ReverseDirection);
Assert.AreEqual(metadata.GrazingEndTime, metadataAfter.GrazingEndTime);
Assert.AreEqual(metadata.LeashPosition, metadataAfter.LeashPosition);
break;
case StayAtLeashPositionMetadata metadata when entityAfter.Metadata is StayAtLeashPositionMetadata metadataAfter:
Assert.AreEqual(metadata.LeashPosition, metadataAfter.LeashPosition);
break;
case EggMetadata metadata when entityAfter.Metadata is EggMetadata metadataAfter:
Assert.AreEqual(metadata.TimeStartHatching, metadataAfter.TimeStartHatching);
Assert.AreEqual(metadata.Progress, metadataAfter.Progress);
break;
default:
Assert.Fail($"Runtime type of {nameof(Entity)}.{nameof(Entity.Metadata)} is not equal: {entity.Metadata?.GetType().Name} - {entityAfter.Metadata?.GetType().Name}");
break;
}
switch (entity)
{
case WorldEntity worldEntity when entityAfter is WorldEntity worldEntityAfter:
Assert.AreEqual(worldEntity.Transform.LocalPosition, worldEntityAfter.Transform.LocalPosition);
Assert.AreEqual(worldEntity.Transform.LocalRotation, worldEntityAfter.Transform.LocalRotation);
Assert.AreEqual(worldEntity.Transform.LocalScale, worldEntityAfter.Transform.LocalScale);
Assert.AreEqual(worldEntity.Level, worldEntityAfter.Level);
Assert.AreEqual(worldEntity.ClassId, worldEntityAfter.ClassId);
Assert.AreEqual(worldEntity.SpawnedByServer, worldEntityAfter.SpawnedByServer);
if (worldEntity.GetType() != worldEntityAfter.GetType())
{
Assert.Fail($"Runtime type of {nameof(WorldEntity)} is not equal: {worldEntity.GetType().Name} - {worldEntityAfter.GetType().Name}");
}
else if (worldEntity.GetType() != typeof(WorldEntity))
{
switch (worldEntity)
{
case PlaceholderGroupWorldEntity placeholderGroupWorldEntity when worldEntityAfter is PlaceholderGroupWorldEntity placeholderGroupWorldEntityAfter:
Assert.AreEqual(placeholderGroupWorldEntity.ComponentIndex, placeholderGroupWorldEntityAfter.ComponentIndex);
break;
case CellRootEntity when worldEntityAfter is CellRootEntity:
break;
case PlacedWorldEntity when worldEntityAfter is PlacedWorldEntity:
break;
case OxygenPipeEntity oxygenPipeEntity when worldEntityAfter is OxygenPipeEntity oxygenPipeEntityAfter:
Assert.AreEqual(oxygenPipeEntity.ParentPipeId, oxygenPipeEntityAfter.ParentPipeId);
Assert.AreEqual(oxygenPipeEntity.RootPipeId, oxygenPipeEntityAfter.RootPipeId);
Assert.AreEqual(oxygenPipeEntity.ParentPosition, oxygenPipeEntityAfter.ParentPosition);
break;
case PrefabPlaceholderEntity prefabPlaceholderEntity when entityAfter is PrefabPlaceholderEntity prefabPlaceholderEntityAfter:
Assert.AreEqual(prefabPlaceholderEntity.ComponentIndex, prefabPlaceholderEntityAfter.ComponentIndex);
break;
case SerializedWorldEntity serializedWorldEntity when entityAfter is SerializedWorldEntity serializedWorldEntityAfter:
Assert.AreEqual(serializedWorldEntity.AbsoluteEntityCell, serializedWorldEntityAfter.AbsoluteEntityCell);
AssertHelper.IsListEqual(serializedWorldEntity.Components.OrderBy(c => c.GetHashCode()), serializedWorldEntityAfter.Components.OrderBy(c => c.GetHashCode()), (c1, c2) => c1.Equals(c2));
Assert.AreEqual(serializedWorldEntity.Layer, serializedWorldEntityAfter.Layer);
Assert.AreEqual(serializedWorldEntity.BatchId, serializedWorldEntityAfter.BatchId);
Assert.AreEqual(serializedWorldEntity.CellId, serializedWorldEntityAfter.CellId);
break;
case GeyserWorldEntity geyserEntity when entityAfter is GeyserWorldEntity geyserEntityAfter:
Assert.AreEqual(geyserEntity.RandomIntervalVarianceMultiplier, geyserEntityAfter.RandomIntervalVarianceMultiplier);
Assert.AreEqual(geyserEntity.StartEruptTime, geyserEntityAfter.StartEruptTime);
break;
case ReefbackEntity reefbackEntity when entityAfter is ReefbackEntity reefbackEntityAfter:
Assert.AreEqual(reefbackEntity.GrassIndex, reefbackEntityAfter.GrassIndex);
Assert.AreEqual(reefbackEntity.OriginalPosition, reefbackEntityAfter.OriginalPosition);
break;
case ReefbackChildEntity reefbackChildEntity when entityAfter is ReefbackChildEntity reefbackChildEntityAfter:
Assert.AreEqual(reefbackChildEntity.Type, reefbackChildEntityAfter.Type);
break;
case CreatureRespawnEntity creatureRespawnEntity when entityAfter is CreatureRespawnEntity creatureRespawnEntityAfter:
Assert.AreEqual(creatureRespawnEntity.SpawnTime, creatureRespawnEntityAfter.SpawnTime);
Assert.AreEqual(creatureRespawnEntity.RespawnTechType, creatureRespawnEntityAfter.RespawnTechType);
Assert.IsTrue(creatureRespawnEntity.AddComponents.SequenceEqual(creatureRespawnEntityAfter.AddComponents));
break;
case GlobalRootEntity globalRootEntity when worldEntityAfter is GlobalRootEntity globalRootEntityAfter:
if (globalRootEntity.GetType() != typeof(GlobalRootEntity))
{
switch (globalRootEntity)
{
case BuildEntity buildEntity when globalRootEntityAfter is BuildEntity buildEntityAfter:
Assert.AreEqual(buildEntity.BaseData, buildEntityAfter.BaseData);
break;
case EscapePodWorldEntity escapePodWorldEntity when globalRootEntityAfter is EscapePodWorldEntity escapePodWorldEntityAfter:
Assert.IsTrue(escapePodWorldEntity.Players.SequenceEqual(escapePodWorldEntityAfter.Players));
break;
case InteriorPieceEntity interiorPieceEntity when globalRootEntityAfter is InteriorPieceEntity interiorPieceEntityAfter:
Assert.AreEqual(interiorPieceEntity.BaseFace, interiorPieceEntityAfter.BaseFace);
break;
case MapRoomEntity mapRoomEntity when globalRootEntityAfter is MapRoomEntity mapRoomEntityAfter:
Assert.AreEqual(mapRoomEntity.Cell, mapRoomEntityAfter.Cell);
break;
case ModuleEntity moduleEntity when globalRootEntityAfter is ModuleEntity moduleEntityAfter:
Assert.AreEqual(moduleEntity.ConstructedAmount, moduleEntityAfter.ConstructedAmount);
Assert.AreEqual(moduleEntity.IsInside, moduleEntityAfter.IsInside);
if (moduleEntity.GetType() != moduleEntityAfter.GetType())
{
Assert.Fail($"Runtime type of {nameof(ModuleEntity)} is not equal: {moduleEntity.GetType().Name} - {moduleEntityAfter.GetType().Name}");
}
switch (moduleEntity)
{
case GhostEntity ghostEntity when moduleEntityAfter is GhostEntity ghostEntityAfter:
Assert.AreEqual(ghostEntity.BaseFace, ghostEntityAfter.BaseFace);
Assert.AreEqual(ghostEntity.BaseData, ghostEntityAfter.BaseData);
break;
}
break;
case MoonpoolEntity moonpoolEntity when globalRootEntityAfter is MoonpoolEntity moonpoolEntityAfter:
Assert.AreEqual(moonpoolEntity.Cell, moonpoolEntityAfter.Cell);
break;
case PlanterEntity when globalRootEntityAfter is PlanterEntity:
break;
case PlayerWorldEntity when globalRootEntityAfter is PlayerWorldEntity:
break;
case VehicleWorldEntity vehicleWorldEntity when globalRootEntityAfter is VehicleWorldEntity vehicleWorldEntityAfter:
Assert.AreEqual(vehicleWorldEntity.SpawnerId, vehicleWorldEntityAfter.SpawnerId);
Assert.AreEqual(vehicleWorldEntity.ConstructionTime, vehicleWorldEntityAfter.ConstructionTime);
break;
case RadiationLeakEntity radiationLeakEntity when globalRootEntityAfter is RadiationLeakEntity radiationLeakEntityAfter:
Assert.AreEqual(radiationLeakEntity.ObjectIndex, radiationLeakEntityAfter.ObjectIndex);
break;
default:
Assert.Fail($"Runtime type of {nameof(GlobalRootEntity)} is not equal even after the check: {worldEntity.GetType().Name} - {globalRootEntityAfter.GetType().Name}");
break;
}
}
break;
default:
Assert.Fail($"Runtime type of {nameof(WorldEntity)} is not equal even after the check: {worldEntity.GetType().Name} - {worldEntityAfter.GetType().Name}");
break;
}
}
break;
case PrefabChildEntity prefabChildEntity when entityAfter is PrefabChildEntity prefabChildEntityAfter:
Assert.AreEqual(prefabChildEntity.ComponentIndex, prefabChildEntityAfter.ComponentIndex);
Assert.AreEqual(prefabChildEntity.ClassId, prefabChildEntityAfter.ClassId);
break;
case InventoryEntity inventoryEntity when entityAfter is InventoryEntity inventoryEntityAfter:
Assert.AreEqual(inventoryEntity.ComponentIndex, inventoryEntityAfter.ComponentIndex);
break;
case InventoryItemEntity inventoryItemEntity when entityAfter is InventoryItemEntity inventoryItemEntityAfter:
Assert.AreEqual(inventoryItemEntity.ClassId, inventoryItemEntityAfter.ClassId);
break;
case PathBasedChildEntity pathBasedChildEntity when entityAfter is PathBasedChildEntity pathBasedChildEntityAfter:
Assert.AreEqual(pathBasedChildEntity.Path, pathBasedChildEntityAfter.Path);
break;
case InstalledBatteryEntity when entityAfter is InstalledBatteryEntity:
break;
case InstalledModuleEntity installedModuleEntity when entityAfter is InstalledModuleEntity installedModuleEntityAfter:
Assert.AreEqual(installedModuleEntity.Slot, installedModuleEntityAfter.Slot);
Assert.AreEqual(installedModuleEntity.ClassId, installedModuleEntityAfter.ClassId);
break;
case BaseLeakEntity baseLeakEntity when entityAfter is BaseLeakEntity baseLeakEntityAfter:
Assert.AreEqual(baseLeakEntity.Health, baseLeakEntityAfter.Health);
Assert.AreEqual(baseLeakEntity.RelativeCell, baseLeakEntityAfter.RelativeCell);
break;
default:
Assert.Fail($"Runtime type of {nameof(Entity)} is not equal: {entity.GetType().Name} - {entityAfter.GetType().Name}");
break;
}
AssertHelper.IsListEqual(entity.ChildEntities.OrderBy(x => x.Id), entityAfter.ChildEntities.OrderBy(x => x.Id), EntityTest);
}
[ClassCleanup]
public static void ClassCleanup()
{
NitroxServiceLocator.EndCurrentLifetimeScope();
if (Directory.Exists(tempSaveFilePath))
{
Directory.Delete(tempSaveFilePath, true);
}
}
}
[AttributeUsage(AttributeTargets.Method)]
public class DynamicWorldDataAfterAttribute : Attribute, ITestDataSource
{
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
{
return WorldPersistenceTest.WorldsDataAfter.Select((t, i) => new object[] { t, WorldPersistenceTest.ServerSerializers[i].GetType().Name });
}
public string GetDisplayName(MethodInfo methodInfo, object[] data)
{
return data != null ? $"{methodInfo.Name} ({data[1]})" : $"{methodInfo.Name} (no-data)";
}
}

View File

@@ -0,0 +1,17 @@
global using Nitrox.Test.Helper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxModel.Helper;
using NitroxModel.Logger;
namespace Nitrox.Test;
[TestClass]
public class SetupAssemblyInitializer
{
[AssemblyInitialize]
public static void AssemblyInit(TestContext context)
{
NitroxEnvironment.Set(NitroxEnvironment.Types.TESTING);
Log.Setup();
}
}

View File

@@ -0,0 +1,15 @@
using Autofac;
using NitroxClient.Debuggers;
using NitroxModel.Core;
using NSubstitute;
namespace Nitrox.Test
{
public class TestAutoFacRegistrar : IAutoFacRegistrar
{
public void RegisterDependencies(ContainerBuilder containerBuilder)
{
containerBuilder.RegisterInstance(Substitute.For<INetworkDebugger>()).As<INetworkDebugger>().SingleInstance();
}
}
}