diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index f77bad7..7a943c1 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -72,7 +72,7 @@ jobs: run: dotnet build NetSdrClient.sln -c Release --no-restore - name: Tests with coverage (OpenCover) run: | - dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` + dotnet test NetSdrClient.sln -c Release --no-build ` /p:CollectCoverage=true ` /p:CoverletOutput=TestResults/coverage.xml ` /p:CoverletOutputFormat=opencover diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs new file mode 100644 index 0000000..f3e99d4 --- /dev/null +++ b/EchoTcpServer/EchoServer.cs @@ -0,0 +1,88 @@ +namespace EchoTcpServer; + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +public class EchoServer +{ + private readonly int _initialPort; + private TcpListener? _listener; + private readonly CancellationTokenSource _cancellationTokenSource; + + public int Port => _listener?.LocalEndpoint is IPEndPoint ep ? ep.Port : _initialPort; + + public EchoServer(int port) + { + _initialPort = port; + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async Task StartAsync() + { + _listener = new TcpListener(IPAddress.Any, _initialPort); + _listener.Start(); + Console.WriteLine($"Server started on port {Port}."); + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + TcpClient client = await _listener.AcceptTcpClientAsync(_cancellationTokenSource.Token); + Console.WriteLine("Client connected."); + + _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) + { + break; + } + } + + Console.WriteLine("Server shutdown."); + } + + private static async Task HandleClientAsync(TcpClient client, CancellationToken token) + { + using (client) + using (NetworkStream stream = client.GetStream()) + { + try + { + byte[] buffer = new byte[8192]; + int bytesRead; + + while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer.AsMemory(), token)) > 0) + { + await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); + Console.WriteLine($"Echoed {bytesRead} bytes to the client."); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Console.WriteLine($"Error: {ex.Message}"); + } + finally + { + Console.WriteLine("Client disconnected."); + } + } + } + + public void Stop() + { + _cancellationTokenSource.Cancel(); + _listener?.Stop(); + _cancellationTokenSource.Dispose(); + Console.WriteLine("Server stopped."); + } +} \ No newline at end of file diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTcpServer/EchoTcpServer.csproj similarity index 100% rename from EchoTcpServer/EchoServer.csproj rename to EchoTcpServer/EchoTcpServer.csproj diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c57..f1971e5 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,88 +1,10 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; +namespace EchoTcpServer; + +using System; using System.Threading.Tasks; -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer +public static class Program { - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; - - - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task StartAsync() - { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); - - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } - - Console.WriteLine("Server shutdown."); - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try - { - byte[] buffer = new byte[8192]; - int bytesRead; - - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) - { - // Echo back the received message - await stream.WriteAsync(buffer, 0, bytesRead, token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - Console.WriteLine($"Error: {ex.Message}"); - } - finally - { - client.Close(); - Console.WriteLine("Client disconnected."); - } - } - } - - public void Stop() - { - _cancellationTokenSource.Cancel(); - _listener.Stop(); - _cancellationTokenSource.Dispose(); - Console.WriteLine("Server stopped."); - } - public static async Task Main(string[] args) { EchoServer server = new EchoServer(5000); @@ -110,64 +32,4 @@ public static async Task Main(string[] args) Console.WriteLine("Sender stopped."); } } -} - - -public class UdpTimedSender : IDisposable -{ - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private Timer _timer; - - public UdpTimedSender(string host, int port) - { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } - - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); - - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } - - ushort i = 0; - - private void SendMessageCallback(object state) - { - try - { - //dummy data - Random rnd = new Random(); - byte[] samples = new byte[1024]; - rnd.NextBytes(samples); - i++; - - byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray(); - var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); - - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port} "); - } - catch (Exception ex) - { - Console.WriteLine($"Error sending message: {ex.Message}"); - } - } - - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } - - public void Dispose() - { - StopSending(); - _udpClient.Dispose(); - } } \ No newline at end of file diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs new file mode 100644 index 0000000..edaa3a5 --- /dev/null +++ b/EchoTcpServer/UdpTimedSender.cs @@ -0,0 +1,79 @@ +namespace EchoTcpServer; + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Security.Cryptography; + +public class UdpTimedSender : IDisposable +{ + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private Timer? _timer; + private ushort _messageIndex = 0; + private bool _disposed; + + public UdpTimedSender(string host, int port) + { + _host = host; + _port = port; + _udpClient = new UdpClient(); + } + + public void StartSending(int intervalMilliseconds) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (_timer != null) + throw new InvalidOperationException("Sender is already running."); + + _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); + } + + private void SendMessageCallback(object? state) + { + try + { + byte[] samples = new byte[1024]; + RandomNumberGenerator.Fill(samples); + _messageIndex++; + + byte[] msg = [0x04, 0x84, .. BitConverter.GetBytes(_messageIndex), .. samples]; + var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); + + _udpClient.Send(msg, msg.Length, endpoint); + Console.WriteLine($"Message sent to {_host}:{_port}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error sending message: {ex.Message}"); + } + } + + public void StopSending() + { + _timer?.Dispose(); + _timer = null; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + StopSending(); + _udpClient.Dispose(); + } + _disposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/EchoTcpServerTests/EchoServerTests.cs b/EchoTcpServerTests/EchoServerTests.cs new file mode 100644 index 0000000..291ea27 --- /dev/null +++ b/EchoTcpServerTests/EchoServerTests.cs @@ -0,0 +1,87 @@ +using EchoTcpServer; +using System.Net.Sockets; +using System.Text; + +namespace EchoTcpServerTests; + +public class EchoServerTests +{ + private EchoServer _server; + private int _dynamicPort; + + [SetUp] + public async Task Setup() + { + _server = new EchoServer(0); + + _ = Task.Run(() => _server.StartAsync()); + + await Task.Delay(100); + + _dynamicPort = _server.Port; + } + + [TearDown] + public void TearDown() + { + _server.Stop(); + } + + [Test] + public async Task Echo_ReturnsSameMsg() + { + // Arrange + using var client = new TcpClient(); + await client.ConnectAsync("127.0.0.1", _dynamicPort); + using var stream = client.GetStream(); + + var messageToSend = "Hello from Unit Test!"; + var bytesToSend = Encoding.UTF8.GetBytes(messageToSend); + var buffer = new byte[1024]; + + // Act + await stream.WriteAsync(bytesToSend); + + var bytesRead = await stream.ReadAsync(buffer); + var receivedMessage = Encoding.UTF8.GetString(buffer, 0, bytesRead); + + // Assert + Assert.That(receivedMessage, Is.EqualTo(messageToSend)); + } + + [Test] + public async Task Echo_MultiMsg_Works() + { + // Arrange + using var client = new TcpClient(); + await client.ConnectAsync("127.0.0.1", _dynamicPort); + using var stream = client.GetStream(); + var buffer = new byte[1024]; + + // Act & Assert 1 + await stream.WriteAsync(Encoding.UTF8.GetBytes("Message 1")); + var bytesRead = await stream.ReadAsync(buffer); + Assert.That(Encoding.UTF8.GetString(buffer, 0, bytesRead), Is.EqualTo("Message 1")); + + // Act & Assert 2 + await stream.WriteAsync(Encoding.UTF8.GetBytes("Message 2")); + bytesRead = await stream.ReadAsync(buffer); + Assert.That(Encoding.UTF8.GetString(buffer, 0, bytesRead), Is.EqualTo("Message 2")); + } + + [Test] + public async Task Echo_ClientDisconnects() + { + // Arrange + using var client = new TcpClient(); + await client.ConnectAsync("127.0.0.1", _dynamicPort); + + // Act + client.Close(); + + await Task.Delay(100); + + // Assert + Assert.Pass(); + } +} \ No newline at end of file diff --git a/EchoTcpServerTests/EchoTcpServerTests.csproj b/EchoTcpServerTests/EchoTcpServerTests.csproj new file mode 100644 index 0000000..7e35cd8 --- /dev/null +++ b/EchoTcpServerTests/EchoTcpServerTests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/EchoTcpServerTests/UdpTimedSenderTests.cs b/EchoTcpServerTests/UdpTimedSenderTests.cs new file mode 100644 index 0000000..24bb57d --- /dev/null +++ b/EchoTcpServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using EchoTcpServer; +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace EchoTcpServerTests; + +public class UdpTimedSenderTests +{ + private int GetFreeUdpPort() + { + using var udp = new UdpClient(0); + return ((System.Net.IPEndPoint)udp.Client.LocalEndPoint!).Port; + } + + [Test] + public async Task StartSending_SendsUdpPackets_WithCorrectHeader() + { + // Arrange + int port = GetFreeUdpPort(); + using var receiver = new UdpClient(port); + using var sender = new UdpTimedSender("127.0.0.1", port); + + // Act + sender.StartSending(50); + + var receiveTask = receiver.ReceiveAsync(); + var completedTask = await Task.WhenAny(receiveTask, Task.Delay(1000)); + + // Assert + Assert.That(completedTask, Is.EqualTo(receiveTask), "Пакети UDP не були отримані вчасно."); + + var udpReceiveResult = receiveTask.Result; + var bytes = udpReceiveResult.Buffer; + + Assert.That(bytes.Length, Is.GreaterThan(0)); + Assert.That(bytes[0], Is.EqualTo(0x04)); + Assert.That(bytes[1], Is.EqualTo(0x84)); + } + + [Test] + public void StartSending_Twice_ThrowsInvalidOperationException() + { + // Arrange + using var sender = new UdpTimedSender("127.0.0.1", GetFreeUdpPort()); + sender.StartSending(1000); + + // Act & Assert + var ex = Assert.Throws(() => sender.StartSending(1000)); + Assert.That(ex.Message, Is.EqualTo("Sender is already running.")); + } + + [Test] + public void StartSending_AfterDispose_ThrowsObjectDisposedException() + { + // Arrange + var sender = new UdpTimedSender("127.0.0.1", GetFreeUdpPort()); + sender.Dispose(); + + // Act & Assert + Assert.Throws(() => sender.StartSending(1000)); + } + + [Test] + public void StopSending_DoesNotThrowException() + { + // Arrange + using var sender = new UdpTimedSender("127.0.0.1", GetFreeUdpPort()); + + // Act & Assert + Assert.DoesNotThrow(() => sender.StopSending()); + } +} \ No newline at end of file diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb..0dd1a24 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -7,7 +7,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientApp", "NetSdrCl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "NetSdrClientAppTests\NetSdrClientAppTests.csproj", "{D0155366-89B4-4BA4-90E2-2ECC8C1EB915}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoTcpServer", "EchoTcpServer\EchoTcpServer.csproj", "{4FE96C93-F645-4C52-A2ED-C4437504A5BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoTcpServerTests", "EchoTcpServerTests\EchoTcpServerTests.csproj", "{6E755217-7132-4AAA-83E9-7013CCABE7C3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -23,10 +25,14 @@ Global {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|Any CPU.Build.0 = Release|Any CPU - {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU + {4FE96C93-F645-4C52-A2ED-C4437504A5BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FE96C93-F645-4C52-A2ED-C4437504A5BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FE96C93-F645-4C52-A2ED-C4437504A5BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FE96C93-F645-4C52-A2ED-C4437504A5BD}.Release|Any CPU.Build.0 = Release|Any CPU + {6E755217-7132-4AAA-83E9-7013CCABE7C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E755217-7132-4AAA-83E9-7013CCABE7C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E755217-7132-4AAA-83E9-7013CCABE7C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E755217-7132-4AAA-83E9-7013CCABE7C3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE