Added some TCP wrapper classes for testability
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Protocols.Modbus.Tcp.Utils;
|
||||||
|
|
||||||
namespace System.IO
|
namespace System.IO
|
||||||
{
|
{
|
||||||
@@ -22,5 +23,23 @@ namespace System.IO
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<byte[]> ReadExpectedBytesAsync(this NetworkStreamWrapper stream, int expectedBytes, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[expectedBytes];
|
||||||
|
int offset = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken);
|
||||||
|
if (count < 1)
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
|
offset += count;
|
||||||
|
}
|
||||||
|
while (offset < expectedBytes && !cancellationToken.IsCancellationRequested);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs
Normal file
32
AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
|
{
|
||||||
|
internal class IPEndPointWrapper
|
||||||
|
{
|
||||||
|
private IPEndPoint _ipEndPoint;
|
||||||
|
|
||||||
|
public IPEndPointWrapper(EndPoint endPoint)
|
||||||
|
{
|
||||||
|
_ipEndPoint = (IPEndPoint)endPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IPEndPoint.Address"/>
|
||||||
|
public virtual IPAddress Address
|
||||||
|
{
|
||||||
|
get => _ipEndPoint.Address;
|
||||||
|
set => _ipEndPoint.Address = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IPEndPoint.Port"/>
|
||||||
|
public virtual int Port
|
||||||
|
{
|
||||||
|
get => _ipEndPoint.Port;
|
||||||
|
set => _ipEndPoint.Port = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,6 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils
|
|||||||
{
|
{
|
||||||
private readonly NetworkStream _stream;
|
private readonly NetworkStream _stream;
|
||||||
|
|
||||||
[Obsolete("Constructor only for mocking on UnitTests!", error: true)]
|
|
||||||
public NetworkStreamWrapper()
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public NetworkStreamWrapper(NetworkStream stream)
|
public NetworkStreamWrapper(NetworkStream stream)
|
||||||
{
|
{
|
||||||
_stream = stream;
|
_stream = stream;
|
||||||
|
|||||||
29
AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs
Normal file
29
AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
|
{
|
||||||
|
internal class SocketWrapper : IDisposable
|
||||||
|
{
|
||||||
|
private Socket _socket;
|
||||||
|
|
||||||
|
public SocketWrapper(Socket socket)
|
||||||
|
{
|
||||||
|
_socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Socket.DualMode" />
|
||||||
|
public virtual bool DualMode
|
||||||
|
{
|
||||||
|
get => _socket.DualMode;
|
||||||
|
set => _socket.DualMode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Socket.IsBound" />
|
||||||
|
public virtual bool IsBound
|
||||||
|
=> _socket.IsBound;
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
=> _socket.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,19 +3,30 @@ using System.Net;
|
|||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Transactions;
|
||||||
|
|
||||||
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
{
|
{
|
||||||
/// <inheritdoc cref="TcpClient" />
|
/// <inheritdoc cref="TcpClient" />
|
||||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable
|
internal class TcpClientWrapper : IDisposable
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private readonly TcpClient _client = new(addressFamily);
|
private readonly TcpClient _client;
|
||||||
|
|
||||||
#endregion Fields
|
#endregion Fields
|
||||||
|
|
||||||
|
public TcpClientWrapper(AddressFamily addressFamily)
|
||||||
|
{
|
||||||
|
_client = new TcpClient(addressFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TcpClientWrapper(TcpClient client)
|
||||||
|
{
|
||||||
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
/// <inheritdoc cref="TcpClient.Connected" />
|
/// <inheritdoc cref="TcpClient.Connected" />
|
||||||
|
|||||||
91
AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs
Normal file
91
AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AMWD.Protocols.Modbus.Tcp.Utils
|
||||||
|
{
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
internal class TcpListenerWrapper : IDisposable
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private TcpListener _tcpListener;
|
||||||
|
|
||||||
|
#endregion Fields
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
public TcpListenerWrapper(IPAddress localaddr, int port)
|
||||||
|
{
|
||||||
|
_tcpListener = new TcpListener(localaddr, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Constructor
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <inheritdoc cref="TcpListener.LocalEndpoint"/>
|
||||||
|
public virtual IPEndPointWrapper LocalIPEndPoint
|
||||||
|
=> new(_tcpListener.LocalEndpoint);
|
||||||
|
|
||||||
|
public virtual SocketWrapper Socket
|
||||||
|
=> new(_tcpListener.Server);
|
||||||
|
|
||||||
|
#endregion Properties
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accepts a pending connection request as a cancellable asynchronous operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This operation will not block. The returned <see cref="Task{TResult}"/> object will complete after the TCP connection has been accepted.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Use the <see cref="TcpClientWrapper.GetStream"/> method to obtain the underlying <see cref="NetworkStreamWrapper"/> of the returned <see cref="TcpClientWrapper"/> in the <see cref="Task{TResult}"/>.
|
||||||
|
/// The <see cref="NetworkStreamWrapper"/> will provide you with methods for sending and receiving with the remote host.
|
||||||
|
/// When you are through with the <see cref="TcpClientWrapper"/>, be sure to call its <see cref="TcpClientWrapper.Close"/> method.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The task object representing the asynchronous operation.
|
||||||
|
/// The <see cref="Task{TResult}.Result"/> property on the task object returns a <see cref="TcpClientWrapper"/> used to send and receive data.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="InvalidOperationException">The listener has not been started with a call to <see cref="Start"/>.</exception>
|
||||||
|
/// <exception cref="SocketException">
|
||||||
|
/// Use the <see cref="SocketException.ErrorCode"/> property to obtain the specific error code.
|
||||||
|
/// When you have obtained this code, you can refer to the
|
||||||
|
/// <see href="https://learn.microsoft.com/en-us/windows/desktop/winsock/windows-sockets-error-codes-2">Windows Sockets version 2 API error code</see>
|
||||||
|
/// documentation for a detailed description of the error.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="OperationCanceledException">The cancellation token was canceled. This exception is stored into the returned task.</exception>
|
||||||
|
public virtual async Task<TcpClientWrapper> AcceptTcpClientAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
var tcpClient = await _tcpListener.AcceptTcpClientAsync(cancellationToken);
|
||||||
|
#else
|
||||||
|
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
|
||||||
|
#endif
|
||||||
|
return new TcpClientWrapper(tcpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Start()
|
||||||
|
=> _tcpListener.Start();
|
||||||
|
|
||||||
|
public virtual void Stop()
|
||||||
|
=> _tcpListener.Stop();
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
_tcpListener.Dispose();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Methods
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -498,7 +498,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp
|
|||||||
|
|
||||||
private ModbusTcpConnection GetTcpConnection()
|
private ModbusTcpConnection GetTcpConnection()
|
||||||
{
|
{
|
||||||
_networkStreamMock = new Mock<NetworkStreamWrapper>();
|
_networkStreamMock = new Mock<NetworkStreamWrapper>(null);
|
||||||
_networkStreamMock
|
_networkStreamMock
|
||||||
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
.Setup(ns => ns.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
.Callback<ReadOnlyMemory<byte>, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))
|
||||||
|
|||||||
Reference in New Issue
Block a user