Added UnitTests for TCP
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
# The image has to use the same version as the .NET UnitTest project
|
|
||||||
image: mcr.microsoft.com/dotnet/sdk:8.0
|
image: mcr.microsoft.com/dotnet/sdk:8.0
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
@@ -44,14 +43,25 @@ test-debug:
|
|||||||
- lnx
|
- lnx
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG == null
|
- if: $CI_COMMIT_TAG == null
|
||||||
# line-coverage
|
|
||||||
#coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
|
|
||||||
# branch-coverage
|
|
||||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||||
script:
|
script:
|
||||||
- dotnet restore --no-cache --force
|
- dotnet restore --no-cache --force
|
||||||
- dotnet test -c Debug --nologo --no-restore
|
- dotnet test -c Debug --nologo --no-restore
|
||||||
|
|
||||||
|
deploy-debug:
|
||||||
|
stage: deploy
|
||||||
|
dependencies:
|
||||||
|
- build-debug
|
||||||
|
- test-debug
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- lnx
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG == null
|
||||||
|
script:
|
||||||
|
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
stage: build
|
stage: build
|
||||||
@@ -85,15 +95,12 @@ test-release:
|
|||||||
- lnx
|
- lnx
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG != null
|
- if: $CI_COMMIT_TAG != null
|
||||||
# line-coverage
|
|
||||||
#coverage: '/Total[^|]*\|\s*([0-9.%]+)/'
|
|
||||||
# branch-coverage
|
|
||||||
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/'
|
||||||
script:
|
script:
|
||||||
- dotnet restore --no-cache --force
|
- dotnet restore --no-cache --force
|
||||||
- dotnet test -c Release --nologo --no-restore
|
- dotnet test -c Release --nologo --no-restore
|
||||||
|
|
||||||
deploy:
|
deploy-release:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
dependencies:
|
dependencies:
|
||||||
- build-release
|
- build-release
|
||||||
|
|||||||
@@ -18,16 +18,13 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
byte[] blob = [HighByte, LowByte];
|
byte[] blob = [HighByte, LowByte];
|
||||||
if (BitConverter.IsLittleEndian)
|
blob.SwapNetworkOrder();
|
||||||
Array.Reverse(blob);
|
|
||||||
|
|
||||||
return BitConverter.ToUInt16(blob, 0);
|
return BitConverter.ToUInt16(blob, 0);
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
byte[] blob = BitConverter.GetBytes(value);
|
byte[] blob = BitConverter.GetBytes(value);
|
||||||
if (BitConverter.IsLittleEndian)
|
blob.SwapNetworkOrder();
|
||||||
Array.Reverse(blob);
|
|
||||||
|
|
||||||
HighByte = blob[0];
|
HighByte = blob[0];
|
||||||
LowByte = blob[1];
|
LowByte = blob[1];
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ namespace AMWD.Protocols.Modbus.Common
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
byte[] blob = [HighByte, LowByte];
|
byte[] blob = [HighByte, LowByte];
|
||||||
if (BitConverter.IsLittleEndian)
|
blob.SwapNetworkOrder();
|
||||||
Array.Reverse(blob);
|
|
||||||
|
|
||||||
return BitConverter.ToUInt16(blob, 0);
|
return BitConverter.ToUInt16(blob, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace System.Collections.Generic
|
|||||||
// ============================================================================================================================= //
|
// ============================================================================================================================= //
|
||||||
// Source: https://git.am-wd.de/am.wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Utilities/AsyncQueue.cs //
|
// Source: https://git.am-wd.de/am.wd/common/-/blob/d4b390ad911ce302cc371bb2121fa9c31db1674a/AMWD.Common/Utilities/AsyncQueue.cs //
|
||||||
// ============================================================================================================================= //
|
// ============================================================================================================================= //
|
||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
internal class AsyncQueue<T>
|
internal class AsyncQueue<T>
|
||||||
{
|
{
|
||||||
private readonly Queue<T> _queue = new();
|
private readonly Queue<T> _queue = new();
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (_disconnectCts != null)
|
if (_disconnectCts != null)
|
||||||
|
{
|
||||||
|
await _reconnectTask;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_disconnectCts = new CancellationTokenSource();
|
_disconnectCts = new CancellationTokenSource();
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disconnectCts.Token, cancellationToken);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disconnectCts.Token, cancellationToken);
|
||||||
@@ -166,6 +169,16 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
if (!IsConnected)
|
if (!IsConnected)
|
||||||
throw new ApplicationException($"Connection is not open");
|
throw new ApplicationException($"Connection is not open");
|
||||||
|
|
||||||
|
if (request?.Count < 1)
|
||||||
|
throw new ArgumentNullException(nameof(request));
|
||||||
|
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
ArgumentNullException.ThrowIfNull(validateResponseComplete);
|
||||||
|
#else
|
||||||
|
if (validateResponseComplete == null)
|
||||||
|
throw new ArgumentNullException(nameof(validateResponseComplete));
|
||||||
|
#endif
|
||||||
|
|
||||||
var item = new RequestQueueItem
|
var item = new RequestQueueItem
|
||||||
{
|
{
|
||||||
Request = [.. request],
|
Request = [.. request],
|
||||||
@@ -178,7 +191,7 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
{
|
{
|
||||||
_requestQueue.Remove(item);
|
_requestQueue.Remove(item);
|
||||||
item.CancellationTokenSource.Dispose();
|
item.CancellationTokenSource.Dispose();
|
||||||
item.TaskCompletionSource.TrySetCanceled();
|
item.TaskCompletionSource.TrySetCanceled(cancellationToken);
|
||||||
item.CancellationTokenRegistration.Dispose();
|
item.CancellationTokenRegistration.Dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -375,7 +388,6 @@ namespace AMWD.Protocols.Modbus.Tcp
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
item.TaskCompletionSource.TrySetException(ex);
|
item.TaskCompletionSource.TrySetException(ex);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
|||||||
{
|
{
|
||||||
private readonly NetworkStream _stream;
|
private readonly NetworkStream _stream;
|
||||||
|
|
||||||
[Obsolete("Constructor only for mocking on UnitTests!")]
|
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
|
||||||
public NetworkStreamWrapper()
|
public NetworkStreamWrapper()
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
|||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
internal class SocketWrapper : IDisposable
|
internal class SocketWrapper : IDisposable
|
||||||
{
|
{
|
||||||
[Obsolete("Constructor only for mocking on UnitTests!")]
|
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
|
||||||
public SocketWrapper()
|
public SocketWrapper()
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.msbuild" Version="6.0.0">
|
<PackageReference Include="coverlet.msbuild" Version="6.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.70" />
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.0" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.0" />
|
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
global using AMWD.Protocols.Modbus.Common;
|
|
||||||
global using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
||||||
global using System;
|
global using System;
|
||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
|
global using AMWD.Protocols.Modbus.Common;
|
||||||
|
global using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|||||||
124
AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpClientTest.cs
Normal file
124
AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpClientTest.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tests.Tcp
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class ModbusTcpClientTest
|
||||||
|
{
|
||||||
|
private Mock<IModbusConnection> _genericConnectionMock;
|
||||||
|
private Mock<ModbusTcpConnection> _tcpConnectionMock;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_genericConnectionMock = new Mock<IModbusConnection>();
|
||||||
|
_tcpConnectionMock = new Mock<ModbusTcpConnection>();
|
||||||
|
_tcpConnectionMock.Setup(c => c.Hostname).Returns("127.0.0.1");
|
||||||
|
_tcpConnectionMock.Setup(c => c.Port).Returns(502);
|
||||||
|
_tcpConnectionMock.Setup(c => c.ReadTimeout).Returns(TimeSpan.FromSeconds(10));
|
||||||
|
_tcpConnectionMock.Setup(c => c.WriteTimeout).Returns(TimeSpan.FromSeconds(20));
|
||||||
|
_tcpConnectionMock.Setup(c => c.ReconnectTimeout).Returns(TimeSpan.FromSeconds(30));
|
||||||
|
_tcpConnectionMock.Setup(c => c.KeepAliveInterval).Returns(TimeSpan.FromSeconds(40));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReturnDefaultValuesForGenericConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = new ModbusTcpClient(_genericConnectionMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string hostname = client.Hostname;
|
||||||
|
int port = client.Port;
|
||||||
|
TimeSpan readTimeout = client.ReadTimeout;
|
||||||
|
TimeSpan writeTimeout = client.WriteTimeout;
|
||||||
|
TimeSpan reconnectTimeout = client.ReconnectTimeout;
|
||||||
|
TimeSpan keepAliveInterval = client.KeepAliveInterval;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNull(hostname);
|
||||||
|
Assert.AreEqual(0, port);
|
||||||
|
Assert.AreEqual(TimeSpan.Zero, readTimeout);
|
||||||
|
Assert.AreEqual(TimeSpan.Zero, writeTimeout);
|
||||||
|
Assert.AreEqual(TimeSpan.Zero, reconnectTimeout);
|
||||||
|
Assert.AreEqual(TimeSpan.Zero, keepAliveInterval);
|
||||||
|
|
||||||
|
_genericConnectionMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldNotSetValuesForGenericConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = new ModbusTcpClient(_genericConnectionMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
client.Hostname = "localhost";
|
||||||
|
client.Port = 205;
|
||||||
|
client.ReadTimeout = TimeSpan.FromSeconds(123);
|
||||||
|
client.WriteTimeout = TimeSpan.FromSeconds(456);
|
||||||
|
client.ReconnectTimeout = TimeSpan.FromSeconds(789);
|
||||||
|
client.KeepAliveInterval = TimeSpan.FromSeconds(321);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_genericConnectionMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReturnValuesForTcpConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = new ModbusTcpClient(_tcpConnectionMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string hostname = client.Hostname;
|
||||||
|
int port = client.Port;
|
||||||
|
TimeSpan readTimeout = client.ReadTimeout;
|
||||||
|
TimeSpan writeTimeout = client.WriteTimeout;
|
||||||
|
TimeSpan reconnectTimeout = client.ReconnectTimeout;
|
||||||
|
TimeSpan keepAliveInterval = client.KeepAliveInterval;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("127.0.0.1", hostname);
|
||||||
|
Assert.AreEqual(502, port);
|
||||||
|
Assert.AreEqual(10, readTimeout.TotalSeconds);
|
||||||
|
Assert.AreEqual(20, writeTimeout.TotalSeconds);
|
||||||
|
Assert.AreEqual(30, reconnectTimeout.TotalSeconds);
|
||||||
|
Assert.AreEqual(40, keepAliveInterval.TotalSeconds);
|
||||||
|
|
||||||
|
_tcpConnectionMock.VerifyGet(c => c.Hostname, Times.Once);
|
||||||
|
_tcpConnectionMock.VerifyGet(c => c.Port, Times.Once);
|
||||||
|
_tcpConnectionMock.VerifyGet(c => c.ReadTimeout, Times.Once);
|
||||||
|
_tcpConnectionMock.VerifyGet(c => c.WriteTimeout, Times.Once);
|
||||||
|
_tcpConnectionMock.VerifyGet(c => c.ReconnectTimeout, Times.Once);
|
||||||
|
_tcpConnectionMock.VerifyGet(c => c.KeepAliveInterval, Times.Once);
|
||||||
|
_tcpConnectionMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldSetValuesForTcpConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = new ModbusTcpClient(_tcpConnectionMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
client.Hostname = "localhost";
|
||||||
|
client.Port = 205;
|
||||||
|
client.ReadTimeout = TimeSpan.FromSeconds(123);
|
||||||
|
client.WriteTimeout = TimeSpan.FromSeconds(456);
|
||||||
|
client.ReconnectTimeout = TimeSpan.FromSeconds(789);
|
||||||
|
client.KeepAliveInterval = TimeSpan.FromSeconds(321);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_tcpConnectionMock.VerifySet(c => c.Hostname = "localhost", Times.Once);
|
||||||
|
_tcpConnectionMock.VerifySet(c => c.Port = 205, Times.Once);
|
||||||
|
_tcpConnectionMock.VerifySet(c => c.ReadTimeout = TimeSpan.FromSeconds(123), Times.Once);
|
||||||
|
_tcpConnectionMock.VerifySet(c => c.WriteTimeout = TimeSpan.FromSeconds(456), Times.Once);
|
||||||
|
_tcpConnectionMock.VerifySet(c => c.ReconnectTimeout = TimeSpan.FromSeconds(789), Times.Once);
|
||||||
|
_tcpConnectionMock.VerifySet(c => c.KeepAliveInterval = TimeSpan.FromSeconds(321), Times.Once);
|
||||||
|
_tcpConnectionMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
722
AMWD.Protocols.Modbus.Tests/Tcp/Utils/ModbusTcpConnectionTest.cs
Normal file
722
AMWD.Protocols.Modbus.Tests/Tcp/Utils/ModbusTcpConnectionTest.cs
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Protocols.Modbus.Common.Contracts;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tests.Tcp.Utils
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class ModbusTcpConnectionTest
|
||||||
|
{
|
||||||
|
private string _hostname = "127.0.0.1";
|
||||||
|
|
||||||
|
private Mock<TcpClientWrapper> _tcpClientMock;
|
||||||
|
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
||||||
|
private Mock<SocketWrapper> _socketMock;
|
||||||
|
|
||||||
|
private bool _clientIsAlwaysConnected;
|
||||||
|
private Queue<bool> _clientIsConnectedQueue;
|
||||||
|
|
||||||
|
private int _clientReceiveTimeout = 1000;
|
||||||
|
private int _clientSendTimeout = 1000;
|
||||||
|
private Task _clientConnectTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
private List<byte[]> _networkRequestCallbacks;
|
||||||
|
|
||||||
|
private Queue<byte[]> _networkResponseQueue;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_clientIsAlwaysConnected = true;
|
||||||
|
_clientIsConnectedQueue = new Queue<bool>();
|
||||||
|
|
||||||
|
_networkRequestCallbacks = [];
|
||||||
|
_networkResponseQueue = new Queue<byte[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldGetAndSetPropertiesOfBaseClient()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_clientIsAlwaysConnected = false;
|
||||||
|
_clientIsConnectedQueue.Enqueue(true);
|
||||||
|
var connection = GetTcpConnection();
|
||||||
|
connection.GetType().GetField("_isConnected", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(connection, true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
connection.ReadTimeout = TimeSpan.FromSeconds(123);
|
||||||
|
connection.WriteTimeout = TimeSpan.FromSeconds(456);
|
||||||
|
|
||||||
|
// Assert - part 1
|
||||||
|
Assert.AreEqual(1, connection.ReadTimeout.TotalSeconds);
|
||||||
|
Assert.AreEqual(1, connection.WriteTimeout.TotalSeconds);
|
||||||
|
Assert.IsTrue(connection.IsConnected);
|
||||||
|
|
||||||
|
// Assert - part 2
|
||||||
|
_tcpClientMock.VerifySet(c => c.ReceiveTimeout = 123000, Times.Once);
|
||||||
|
_tcpClientMock.VerifySet(c => c.SendTimeout = 456000, Times.Once);
|
||||||
|
|
||||||
|
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
||||||
|
_tcpClientMock.VerifyGet(c => c.SendTimeout, Times.Once);
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Connected, Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataTestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
[ExpectedException(typeof(ArgumentNullException))]
|
||||||
|
public void ShouldThrowArumentNullExceptionForInvalidHostname(string hostname)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetTcpConnection();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
connection.Hostname = hostname;
|
||||||
|
|
||||||
|
// Assert - ArgumentNullException
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataTestMethod]
|
||||||
|
[DataRow(0)]
|
||||||
|
[DataRow(65536)]
|
||||||
|
[ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||||
|
public void ShouldThrowArumentOutOfRangeExceptionForInvalidPort(int port)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetTcpConnection();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
connection.Port = port;
|
||||||
|
|
||||||
|
// Assert - ArgumentOutOfRangeException
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldConnectAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(connection.IsConnected);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(IPAddress.Loopback, 502, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Connected, Times.Exactly(2));
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Client, Times.Exactly(3));
|
||||||
|
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, false), Times.Once);
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 0), Times.Once);
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 0), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldOnlyConnectAsyncOnce()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(connection.IsConnected);
|
||||||
|
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Connected, Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(ApplicationException))]
|
||||||
|
public async Task ShouldThrowApplicationExceptionHostnameNotResolvable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_hostname = "device.internal";
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
|
||||||
|
// Assert - ApplicationException
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldRetryConnectAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_clientIsAlwaysConnected = false;
|
||||||
|
_clientIsConnectedQueue.Enqueue(false);
|
||||||
|
_clientIsConnectedQueue.Enqueue(true);
|
||||||
|
_clientIsConnectedQueue.Enqueue(true);
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(connection.IsConnected);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Close(), Times.Exactly(2));
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(IPAddress.Loopback, 502, It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
|
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Exactly(2));
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Connected, Times.Exactly(3));
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Client, Times.Exactly(3));
|
||||||
|
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, false), Times.Once);
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 0), Times.Once);
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 0), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(SocketException))]
|
||||||
|
public async Task ShouldThrowSocketExceptionOnConnectAsyncForNoReconnect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_clientIsAlwaysConnected = false;
|
||||||
|
_clientIsConnectedQueue.Enqueue(false);
|
||||||
|
var connection = GetTcpConnection();
|
||||||
|
connection.ReconnectTimeout = TimeSpan.Zero;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
|
||||||
|
// Assert - SocketException
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldDisconnectAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.DisconnectAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(connection.IsConnected);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldOnlyDisconnectAsyncOnce()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
await connection.DisconnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.DisconnectAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(connection.IsConnected);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldCallDisconnectOnDispose()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
connection.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldAllowMultipleDispose()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
connection.Dispose();
|
||||||
|
connection.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(ApplicationException))]
|
||||||
|
public async Task ShouldThrowApplicationExceptionOnInvokeAsyncWhileNotConnected()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.InvokeAsync(null, null);
|
||||||
|
|
||||||
|
// Assert - ApplicationException
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataTestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow(new byte[0])]
|
||||||
|
[ExpectedException(typeof(ArgumentNullException))]
|
||||||
|
public async Task ShouldThrowArgumentNullExceptionOnInvokeAsyncForRequest(byte[] request)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.InvokeAsync(request, null);
|
||||||
|
|
||||||
|
// Assert - ArgumentNullException
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(ArgumentNullException))]
|
||||||
|
public async Task ShouldThrowArgumentNullExceptionOnInvokeAsyncForMissingValidation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
byte[] request = new byte[1];
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await connection.InvokeAsync(request, null);
|
||||||
|
|
||||||
|
// Assert - ArgumentNullException
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldInvokeAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_networkResponseQueue.Enqueue([9, 8, 7]);
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await connection.InvokeAsync(request, validation);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1, _networkRequestCallbacks.Count);
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(new byte[] { 9, 8, 7 }, response.ToArray());
|
||||||
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks[0]);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_networkStreamMock.Verify(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_networkStreamMock.Verify(ns => ns.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(EndOfStreamException))]
|
||||||
|
public async Task ShouldThrowEndOfStreamOnInvokeAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_ = await connection.InvokeAsync(request, validation);
|
||||||
|
|
||||||
|
// Assert - EndOfStreamException
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TaskCanceledException))]
|
||||||
|
public async Task ShouldCancelOnInvokeAsyncOnDisconnect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(new ValueTask(Task.Delay(100)));
|
||||||
|
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = connection.InvokeAsync(request, validation);
|
||||||
|
await connection.DisconnectAsync();
|
||||||
|
await task;
|
||||||
|
|
||||||
|
// Assert - TaskCanceledException
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(TaskCanceledException))]
|
||||||
|
public async Task ShouldCancelOnInvokeAsyncOnAbort()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(new ValueTask(Task.Delay(100)));
|
||||||
|
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = connection.InvokeAsync(request, validation, cts.Token);
|
||||||
|
cts.Cancel();
|
||||||
|
await task;
|
||||||
|
|
||||||
|
// Assert - TaskCanceledException
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataTestMethod]
|
||||||
|
[DataRow(typeof(IOException))]
|
||||||
|
[DataRow(typeof(SocketException))]
|
||||||
|
[DataRow(typeof(TimeoutException))]
|
||||||
|
[DataRow(typeof(InvalidOperationException))]
|
||||||
|
public async Task ShouldReconnectOnInvokeAsyncForExceptionType(Type exceptionType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_networkResponseQueue.Enqueue([9, 8, 7]);
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||||
|
.ThrowsAsync((Exception)Activator.CreateInstance(exceptionType));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.InvokeAsync(request, validation);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Assert - part 1
|
||||||
|
Assert.IsInstanceOfType(ex, exceptionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert - part 2
|
||||||
|
Assert.AreEqual(1, _networkRequestCallbacks.Count);
|
||||||
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks[0]);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.ConnectAsync(IPAddress.Loopback, 502, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_tcpClientMock.VerifyGet(c => c.ReceiveTimeout, Times.Once);
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Connected, Times.Exactly(2));
|
||||||
|
_tcpClientMock.VerifyGet(c => c.Client, Times.Exactly(3));
|
||||||
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_networkStreamMock.Verify(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, false), Times.Once);
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 0), Times.Once);
|
||||||
|
_socketMock.Verify(s => s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 0), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldReturnWithUnknownExceptionOnInvokeAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||||
|
.ThrowsAsync(new NotImplementedException());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.InvokeAsync(request, validation);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Assert - part 1
|
||||||
|
Assert.IsInstanceOfType(ex, typeof(NotImplementedException));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert - part 2
|
||||||
|
Assert.AreEqual(1, _networkRequestCallbacks.Count);
|
||||||
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks[0]);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Connected, Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_networkStreamMock.Verify(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldRemoveRequestFromQueueOnInvokeAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_networkResponseQueue.Enqueue([9, 8, 7]);
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||||
|
.Returns(new ValueTask(Task.Delay(100)));
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var taskToComplete = connection.InvokeAsync(request, validation);
|
||||||
|
|
||||||
|
var taskToCancel = connection.InvokeAsync(request, validation, cts.Token);
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
var response = await taskToComplete;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await taskToCancel;
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{ /* expected exception */ }
|
||||||
|
|
||||||
|
Assert.AreEqual(1, _networkRequestCallbacks.Count);
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(new byte[] { 9, 8, 7 }, response.ToArray());
|
||||||
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks[0]);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(2));
|
||||||
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_networkStreamMock.Verify(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_networkStreamMock.Verify(ns => ns.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldCancelQueuedRequestOnDisconnect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_networkResponseQueue.Enqueue([9, 8, 7]);
|
||||||
|
byte[] request = [1, 2, 3];
|
||||||
|
var validation = new Func<IReadOnlyList<byte>, bool>(_ => true);
|
||||||
|
|
||||||
|
var connection = GetConnection();
|
||||||
|
await connection.ConnectAsync();
|
||||||
|
ClearInvocations();
|
||||||
|
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||||
|
.Returns(new ValueTask(Task.Delay(100)));
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var taskToCancel = connection.InvokeAsync(request, validation);
|
||||||
|
var taskToDequeue = connection.InvokeAsync(request, validation);
|
||||||
|
await connection.DisconnectAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await taskToCancel;
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
/* expected exception */
|
||||||
|
Assert.AreNotEqual(CancellationToken.None, ex.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await taskToDequeue;
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
/* expected exception */
|
||||||
|
Assert.AreEqual(CancellationToken.None, ex.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(1, _networkRequestCallbacks.Count);
|
||||||
|
CollectionAssert.AreEqual(request, _networkRequestCallbacks[0]);
|
||||||
|
|
||||||
|
_tcpClientMock.Verify(c => c.Connected, Times.Exactly(2));
|
||||||
|
_tcpClientMock.Verify(c => c.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Close(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(ns => ns.FlushAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_networkStreamMock.Verify(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
|
||||||
|
_socketMock.VerifyNoOtherCalls();
|
||||||
|
_tcpClientMock.VerifyNoOtherCalls();
|
||||||
|
_networkStreamMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IModbusConnection GetConnection()
|
||||||
|
=> GetTcpConnection();
|
||||||
|
|
||||||
|
private ModbusTcpConnection GetTcpConnection()
|
||||||
|
{
|
||||||
|
_networkStreamMock = new Mock<NetworkStreamWrapper>();
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||||
|
.Returns(ValueTask.CompletedTask);
|
||||||
|
_networkStreamMock
|
||||||
|
.Setup(ns => ns.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns<Memory<byte>, CancellationToken>((buffer, _) =>
|
||||||
|
{
|
||||||
|
if (_networkResponseQueue.TryDequeue(out byte[] bytes))
|
||||||
|
{
|
||||||
|
bytes.CopyTo(buffer);
|
||||||
|
return ValueTask.FromResult(bytes.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.FromResult(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
_socketMock = new Mock<SocketWrapper>();
|
||||||
|
|
||||||
|
_tcpClientMock = new Mock<TcpClientWrapper>();
|
||||||
|
_tcpClientMock.Setup(c => c.Client).Returns(() => _socketMock.Object);
|
||||||
|
_tcpClientMock.Setup(c => c.Connected).Returns(() => _clientIsAlwaysConnected || _clientIsConnectedQueue.Dequeue());
|
||||||
|
_tcpClientMock.Setup(c => c.ReceiveTimeout).Returns(() => _clientReceiveTimeout);
|
||||||
|
_tcpClientMock.Setup(c => c.SendTimeout).Returns(() => _clientSendTimeout);
|
||||||
|
|
||||||
|
_tcpClientMock
|
||||||
|
.Setup(c => c.ConnectAsync(It.IsAny<IPAddress>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(() => _clientConnectTask);
|
||||||
|
|
||||||
|
_tcpClientMock
|
||||||
|
.Setup(c => c.GetStream())
|
||||||
|
.Returns(() => _networkStreamMock.Object);
|
||||||
|
|
||||||
|
var connection = new ModbusTcpConnection
|
||||||
|
{
|
||||||
|
Hostname = _hostname,
|
||||||
|
Port = 502
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace real TCP client with mock
|
||||||
|
var clientField = connection.GetType().GetField("_client", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
(clientField.GetValue(connection) as TcpClientWrapper)?.Dispose();
|
||||||
|
clientField.SetValue(connection, _tcpClientMock.Object);
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearInvocations()
|
||||||
|
{
|
||||||
|
_networkStreamMock.Invocations.Clear();
|
||||||
|
_socketMock.Invocations.Clear();
|
||||||
|
_tcpClientMock.Invocations.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user