Added some TCP wrapper classes for testability

This commit is contained in:
2025-01-28 13:58:01 +01:00
parent 05759f8e12
commit 4ef7500c3b
7 changed files with 185 additions and 7 deletions

View File

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

View 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
}
}

View File

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

View 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();
}
}

View File

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

View 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
}
}

View File

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