Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion EchoTcpServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
sender.StartSending(5000);

Console.WriteLine("Press 'q' to quit...");
while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { }
while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q)
{
// wait for 'q' key press
}

sender.StopSending();
server.Stop();
Expand Down
6 changes: 5 additions & 1 deletion EchoTcpServer/UdpTimedSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

namespace EchoTcpServer;

public class UdpTimedSender : IDisposable
public sealed class UdpTimedSender : IDisposable
{
private readonly string _host;
private readonly int _port;
private readonly UdpClient _udpClient;
private Timer? _timer;
private ushort _sequence = 0;
private bool _disposed;

public UdpTimedSender(string host, int port)
{
Expand Down Expand Up @@ -53,7 +54,10 @@ public void StopSending()

public void Dispose()
{
if (_disposed) return;
_disposed = true;
StopSending();
_udpClient.Dispose();
GC.SuppressFinalize(this);
}
}
4 changes: 2 additions & 2 deletions NetSdrClientApp/NetSdrClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
await SendTcpRequest(msg);
}

private void _udpClient_MessageReceived(object? sender, byte[] e)

Check warning on line 116 in NetSdrClientApp/NetSdrClient.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Make '_udpClient_MessageReceived' a static method.

Check warning on line 116 in NetSdrClientApp/NetSdrClient.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Make '_udpClient_MessageReceived' a static method.
{
NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body);
var samples = NetSdrMessageHelper.GetSamples(16, body);
Expand All @@ -130,9 +130,9 @@
}
}

private TaskCompletionSource<byte[]> responseTaskSource;
private TaskCompletionSource<byte[]>? responseTaskSource;

private async Task<byte[]> SendTcpRequest(byte[] msg)
private async Task<byte[]?> SendTcpRequest(byte[] msg)
{
if (!_tcpClient.Connected)
{
Expand All @@ -152,7 +152,7 @@

private void _tcpClient_MessageReceived(object? sender, byte[] e)
{
//TODO: add Unsolicited messages handling here

Check warning on line 155 in NetSdrClientApp/NetSdrClient.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Complete the task associated to this 'TODO' comment.

Check warning on line 155 in NetSdrClientApp/NetSdrClient.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Complete the task associated to this 'TODO' comment.
if (responseTaskSource != null)
{
responseTaskSource.SetResult(e);
Expand Down
14 changes: 8 additions & 6 deletions NetSdrClientApp/Networking/IUdpClient.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@

public interface IUdpClient
namespace NetSdrClientApp.Networking
{
event EventHandler<byte[]>? MessageReceived;
public interface IUdpClient
{
event EventHandler<byte[]>? MessageReceived;

Task StartListeningAsync();
Task StartListeningAsync();

void StopListening();
void Exit();
void StopListening();
void Exit();
}
}
12 changes: 7 additions & 5 deletions NetSdrClientApp/Networking/TcpClientWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class TcpClientWrapper : ITcpClient
private readonly int _port;
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private CancellationTokenSource _cts;
private CancellationTokenSource? _cts;

public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null;

Expand All @@ -40,6 +40,7 @@ public void Connect()

try
{
_cts?.Dispose();
_cts = new CancellationTokenSource();
_tcpClient.Connect(_host, _port);
_stream = _tcpClient.GetStream();
Expand All @@ -60,6 +61,7 @@ public void Disconnect()
_stream?.Close();
_tcpClient?.Close();

_cts?.Dispose();
_cts = null;
_tcpClient = null;
_stream = null;
Expand All @@ -86,7 +88,7 @@ private async Task WriteToStreamAsync(byte[] data)
if (Connected && _stream != null && _stream.CanWrite)
{
Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}"));
await _stream.WriteAsync(data, 0, data.Length);
await _stream.WriteAsync(data.AsMemory(0, data.Length));
}
else
{
Expand All @@ -102,18 +104,18 @@ private async Task StartListeningAsync()
{
Console.WriteLine($"Starting listening for incomming messages.");

while (!_cts.Token.IsCancellationRequested)
while (_cts != null && !_cts.Token.IsCancellationRequested)
{
byte[] buffer = new byte[8194];

int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token);
int bytesRead = await _stream.ReadAsync(buffer.AsMemory(0, buffer.Length), _cts.Token);
if (bytesRead > 0)
{
MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray());
}
}
}
catch (OperationCanceledException ex)
catch (OperationCanceledException)
{
//empty
}
Expand Down
19 changes: 8 additions & 11 deletions NetSdrClientApp/Networking/UdpClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -23,6 +21,7 @@ public UdpClientWrapper(int port)

public async Task StartListeningAsync()
{
_cts?.Dispose();
_cts = new CancellationTokenSource();
Console.WriteLine("Start listening for UDP messages...");

Expand All @@ -37,7 +36,7 @@ public async Task StartListeningAsync()
Console.WriteLine($"Received from {result.RemoteEndPoint}");
}
}
catch (OperationCanceledException ex)
catch (OperationCanceledException)
{
//empty
}
Expand All @@ -63,14 +62,12 @@ public void StopListening()

public void Exit() => StopListening();

public override int GetHashCode()
{
var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}";

using var md5 = MD5.Create();
var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload));
public override bool Equals(object? obj) =>
obj is UdpClientWrapper other &&
_localEndPoint.Address.Equals(other._localEndPoint.Address) &&
_localEndPoint.Port == other._localEndPoint.Port;

return BitConverter.ToInt32(hash, 0);
}
public override int GetHashCode() =>
HashCode.Combine(nameof(UdpClientWrapper), _localEndPoint.Address, _localEndPoint.Port);
}
}
27 changes: 15 additions & 12 deletions NetSdrClientAppTests/NetSdrMessageHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ public void GetControlItemMessageTest()
var actualCode = BitConverter.ToInt16(codeBytes.ToArray());

//Assert
Assert.That(headerBytes.Count(), Is.EqualTo(2));
Assert.That(msg.Length, Is.EqualTo(actualLength));
Assert.That(type, Is.EqualTo(actualType));

Assert.That(actualCode, Is.EqualTo((short)code));

Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength));
using (Assert.EnterMultipleScope())
{
Assert.That(headerBytes.ToArray(), Has.Length.EqualTo(2));
Assert.That(msg.Length, Is.EqualTo(actualLength));
Assert.That(type, Is.EqualTo(actualType));
Assert.That(actualCode, Is.EqualTo((short)code));
Assert.That(parametersBytes.ToArray(), Has.Length.EqualTo(parametersLength));
}
}

[Test]
Expand All @@ -57,11 +58,13 @@ public void GetDataItemMessageTest()
var actualLength = num - ((int)actualType << 13);

//Assert
Assert.That(headerBytes.Count(), Is.EqualTo(2));
Assert.That(msg.Length, Is.EqualTo(actualLength));
Assert.That(type, Is.EqualTo(actualType));

Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength));
using (Assert.EnterMultipleScope())
{
Assert.That(headerBytes.ToArray(), Has.Length.EqualTo(2));
Assert.That(msg.Length, Is.EqualTo(actualLength));
Assert.That(type, Is.EqualTo(actualType));
Assert.That(parametersBytes.ToArray(), Has.Length.EqualTo(parametersLength));
}
}

[Test]
Expand Down
160 changes: 160 additions & 0 deletions NetSdrClientAppTests/NetworkingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,57 @@ public void Exit_DelegatesToStopListening_DoesNotThrow()
// Act & Assert
Assert.DoesNotThrow((Action)(() => wrapper.Exit()));
}

[Test]
public void Equals_SamePort_ReturnsTrue()
{
var a = new UdpClientWrapper(12345);
var b = new UdpClientWrapper(12345);
Assert.That(a, Is.EqualTo(b));
}

[Test]
public void Equals_DifferentPort_ReturnsFalse()
{
var a = new UdpClientWrapper(12345);
var b = new UdpClientWrapper(12346);
Assert.That(a, Is.Not.EqualTo(b));
}

[Test]
public void Equals_Null_ReturnsFalse()
{
var a = new UdpClientWrapper(12345);
Assert.That(a, Is.Not.Null);
}

[Test]
public void GetHashCode_SamePort_ReturnsSameHash()
{
var a = new UdpClientWrapper(12345);
var b = new UdpClientWrapper(12345);
Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode()));
}

[Test]
public void Equals_DifferentType_ReturnsFalse()
{
var a = new UdpClientWrapper(12345);
object differentType = "not a wrapper";

Assert.That(a, Is.Not.EqualTo(differentType));
}

[Test]
public async Task StartListeningAsync_ThenStop_DisposesCtsProperly()
{
var wrapper = new UdpClientWrapper(0);
var listenTask = wrapper.StartListeningAsync();
await Task.Delay(20);
wrapper.StopListening();
await listenTask;
Assert.That(listenTask.IsCompletedSuccessfully, Is.True);
}
}

public class TcpClientWrapperTests
Expand All @@ -46,4 +97,113 @@ public void SendMessageAsync_String_ThrowsWhenNotConnected()
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>((Func<Task>)(() => wrapper.SendMessageAsync("hello")));
}

[Test]
public void Connected_WhenNotConnected_ReturnsFalse()
{
var wrapper = new TcpClientWrapper("localhost", 19999);
Assert.That(wrapper.Connected, Is.False);
}

[Test]
public void Disconnect_WhenNotConnected_DoesNotThrow()
{
var wrapper = new TcpClientWrapper("localhost", 19999);
Assert.DoesNotThrow((Action)(() => wrapper.Disconnect()));
}

private static System.Net.Sockets.TcpListener StartListener(out int port)
{
var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
listener.Start();
port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
return listener;
}

[Test]
public async Task Connect_SendMessage_Disconnect_HappyPath()
{
// Arrange
var listener = StartListener(out int port);
try
{
_ = listener.AcceptTcpClientAsync();
var wrapper = new TcpClientWrapper("127.0.0.1", port);

// Act
wrapper.Connect();
await Task.Delay(50);

// Assert
Assert.That(wrapper.Connected, Is.True);
Assert.DoesNotThrowAsync((Func<Task>)(() => wrapper.SendMessageAsync([0x01])));
wrapper.Disconnect();
Assert.That(wrapper.Connected, Is.False);
}
finally
{
listener.Stop();
}
}

[Test]
public async Task MessageReceived_RaisedWhenServerSendsData()
{
// Arrange
var listener = StartListener(out int port);
var received = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
try
{
_ = Task.Run(async () =>
{
var client = await listener.AcceptTcpClientAsync();
var stream = client.GetStream();
await stream.WriteAsync(new byte[] { 0x01, 0x02, 0x03 });
});

var wrapper = new TcpClientWrapper("127.0.0.1", port);
wrapper.MessageReceived += (_, data) => received.TrySetResult(data);

// Act
wrapper.Connect();
var result = await received.Task.WaitAsync(TimeSpan.FromSeconds(3));

// Assert
Assert.That(result, Is.EqualTo(new byte[] { 0x01, 0x02, 0x03 }));
wrapper.Disconnect();
}
finally
{
listener.Stop();
}
}

[Test]
public async Task StartListening_ServerClosesConnection_ListenerLoopHandlesException()
{
// Arrange — server closes immediately, triggering catch(Exception) in the read loop
var listener = StartListener(out int port);
var serverClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
try
{
_ = Task.Run(async () =>
{
var client = await listener.AcceptTcpClientAsync();
await Task.Delay(30);
client.Close();
serverClosed.TrySetResult();
});

var wrapper = new TcpClientWrapper("127.0.0.1", port);

// Act & Assert — no exception propagated to caller
wrapper.Connect();
await serverClosed.Task.WaitAsync(TimeSpan.FromSeconds(3));
Assert.Pass();
}
finally
{
listener.Stop();
}
}
}
Loading