From 7ca46f321cd308182148a7e053928e309ecfee66 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 11:37:31 +0300 Subject: [PATCH 01/27] lab1 --- .github/workflows/sonarcloud.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e784069..73fa9dc 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -37,6 +37,8 @@ on: permissions: pull-requests: read # allows SonarCloud to decorate PRs with analysis results + contents: read + jobs: sonar-check: @@ -56,8 +58,8 @@ jobs: dotnet tool install --global dotnet-sonarscanner echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` - /k:"ppanchen_NetSdrClient" ` - /o:"ppanchen" ` + /k:"slavik22_ReengineeringCourse" ` + /o:"slavik22" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` From 4fb8cef3e61254a5c037a63fda488a5813e58210 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 11:58:31 +0300 Subject: [PATCH 02/27] lab2 --- NetSdrClientApp/Messages/NetSdrMessageHelper.cs | 8 +------- NetSdrClientApp/NetSdrClient.cs | 8 -------- NetSdrClientApp/Networking/ITcpClient.cs | 9 +-------- NetSdrClientApp/Networking/TcpClientWrapper.cs | 9 +-------- NetSdrClientApp/Networking/UdpClientWrapper.cs | 5 +---- 5 files changed, 4 insertions(+), 35 deletions(-) diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 0d69b4d..43ee883 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.PortableExecutable; -using System.Text; -using System.Threading.Tasks; - + namespace NetSdrClientApp.Messages { //TODO: analyze possible use of [StructLayout] for better performance and readability diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c05..022acc1 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,14 +1,6 @@ using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; using static NetSdrClientApp.Messages.NetSdrMessageHelper; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace NetSdrClientApp { diff --git a/NetSdrClientApp/Networking/ITcpClient.cs b/NetSdrClientApp/Networking/ITcpClient.cs index 3470b5d..e779e95 100644 --- a/NetSdrClientApp/Networking/ITcpClient.cs +++ b/NetSdrClientApp/Networking/ITcpClient.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace NetSdrClientApp.Networking +namespace NetSdrClientApp.Networking { public interface ITcpClient { diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e..2d4e555 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -1,12 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Sockets; +using System.Net.Sockets; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace NetSdrClientApp.Networking { diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b79..04091a0 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,10 +1,7 @@ -using System; -using System.Net; +using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; -using System.Threading; -using System.Threading.Tasks; public class UdpClientWrapper : IUdpClient { From 78b60e6f6f03d15eed9e80c2b93a9ab6d474fcfa Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:13:20 +0300 Subject: [PATCH 03/27] make private fields readonly --- NetSdrClientApp/NetSdrClient.cs | 4 ++-- NetSdrClientApp/Networking/TcpClientWrapper.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index 022acc1..97f52c3 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -6,8 +6,8 @@ namespace NetSdrClientApp { public class NetSdrClient { - private ITcpClient _tcpClient; - private IUdpClient _udpClient; + private readonly ITcpClient _tcpClient; + private readonly IUdpClient _udpClient; public bool IQStarted { get; set; } diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 2d4e555..afbe43d 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -5,8 +5,8 @@ namespace NetSdrClientApp.Networking { public class TcpClientWrapper : ITcpClient { - private string _host; - private int _port; + private readonly string _host; + private readonly int _port; private TcpClient? _tcpClient; private NetworkStream? _stream; private CancellationTokenSource _cts; From ef1262c6604320e578f756a6f0291c537f1f96d5 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:19:36 +0300 Subject: [PATCH 04/27] fix: make private fields readonly (S2933) --- NetSdrClientApp/NetSdrClient.cs | 8 ++++---- NetSdrClientApp/Networking/TcpClientWrapper.cs | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index 97f52c3..1b9a04f 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -58,7 +58,7 @@ public async Task StartIQAsync() return; } -; var iqDataMode = (byte)0x80; + var iqDataMode = (byte)0x80; var start = (byte)0x02; var fifo16bitCaptureMode = (byte)0x01; var n = (byte)1; @@ -108,7 +108,7 @@ public async Task ChangeFrequencyAsync(long hz, int channel) private void _udpClient_MessageReceived(object? sender, byte[] e) { - NetSdrMessageHelper.TranslateMessage(e, out MsgTypes type, out ControlItemCodes code, out ushort sequenceNum, out byte[] body); + NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body); var samples = NetSdrMessageHelper.GetSamples(16, body); Console.WriteLine($"Samples recieved: " + body.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); @@ -123,9 +123,9 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) } } - private TaskCompletionSource responseTaskSource; + private TaskCompletionSource? responseTaskSource; - private async Task SendTcpRequest(byte[] msg) + private async Task SendTcpRequest(byte[] msg) { if (!_tcpClient.Connected) { diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index afbe43d..7500897 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -9,7 +9,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; @@ -50,6 +50,7 @@ public void Disconnect() if (Connected) { _cts?.Cancel(); + _cts?.Dispose(); _stream?.Close(); _tcpClient?.Close(); @@ -110,7 +111,7 @@ private async Task StartListeningAsync() } } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { //empty } From a07a5418b7da28ea1e019ed970a83e7a31d1dd05 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:23:11 +0300 Subject: [PATCH 05/27] fix: dispose CancellationTokenSource to prevent resource leak (S2930) --- NetSdrClientApp/Networking/UdpClientWrapper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 04091a0..7646972 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -47,6 +47,7 @@ public void StopListening() try { _cts?.Cancel(); + _cts?.Dispose(); _udpClient?.Close(); Console.WriteLine("Stopped listening for UDP messages."); } From e968a0f43bac820fd9553f9a612e3ce724fd5546 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:23:20 +0300 Subject: [PATCH 06/27] fix: remove unused exception variable in empty catch (S2486) --- NetSdrClientApp/Networking/UdpClientWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 7646972..c2c9e18 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -32,7 +32,7 @@ public async Task StartListeningAsync() Console.WriteLine($"Received from {result.RemoteEndPoint}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { //empty } From 3ca003c66f605fab7b90d0f931859849671ee281 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:23:52 +0300 Subject: [PATCH 07/27] fix: move IUdpClient and UdpClientWrapper into named namespace (S3903) --- NetSdrClientApp/Networking/IUdpClient.cs | 17 ++- .../Networking/UdpClientWrapper.cs | 123 +++++++++--------- 2 files changed, 73 insertions(+), 67 deletions(-) diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs index 1b9f931..053262b 100644 --- a/NetSdrClientApp/Networking/IUdpClient.cs +++ b/NetSdrClientApp/Networking/IUdpClient.cs @@ -1,10 +1,13 @@ - -public interface IUdpClient + +namespace NetSdrClientApp.Networking { - event EventHandler? MessageReceived; + public interface IUdpClient + { + event EventHandler? MessageReceived; - Task StartListeningAsync(); + Task StartListeningAsync(); - void StopListening(); - void Exit(); -} \ No newline at end of file + void StopListening(); + void Exit(); + } +} diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index c2c9e18..941a33a 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,83 +1,86 @@ -using System.Net; +using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; -public class UdpClientWrapper : IUdpClient +namespace NetSdrClientApp.Networking { - private readonly IPEndPoint _localEndPoint; - private CancellationTokenSource? _cts; - private UdpClient? _udpClient; - - public event EventHandler? MessageReceived; - - public UdpClientWrapper(int port) + public class UdpClientWrapper : IUdpClient { - _localEndPoint = new IPEndPoint(IPAddress.Any, port); - } + private readonly IPEndPoint _localEndPoint; + private CancellationTokenSource? _cts; + private UdpClient? _udpClient; - public async Task StartListeningAsync() - { - _cts = new CancellationTokenSource(); - Console.WriteLine("Start listening for UDP messages..."); + public event EventHandler? MessageReceived; - try + public UdpClientWrapper(int port) { - _udpClient = new UdpClient(_localEndPoint); - while (!_cts.Token.IsCancellationRequested) + _localEndPoint = new IPEndPoint(IPAddress.Any, port); + } + + public async Task StartListeningAsync() + { + _cts = new CancellationTokenSource(); + Console.WriteLine("Start listening for UDP messages..."); + + try { - UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); - MessageReceived?.Invoke(this, result.Buffer); + _udpClient = new UdpClient(_localEndPoint); + while (!_cts.Token.IsCancellationRequested) + { + UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); + MessageReceived?.Invoke(this, result.Buffer); - Console.WriteLine($"Received from {result.RemoteEndPoint}"); + Console.WriteLine($"Received from {result.RemoteEndPoint}"); + } + } + catch (OperationCanceledException) + { + //empty + } + catch (Exception ex) + { + Console.WriteLine($"Error receiving message: {ex.Message}"); } } - catch (OperationCanceledException) - { - //empty - } - catch (Exception ex) - { - Console.WriteLine($"Error receiving message: {ex.Message}"); - } - } - public void StopListening() - { - try + public void StopListening() { - _cts?.Cancel(); - _cts?.Dispose(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) - { - Console.WriteLine($"Error while stopping: {ex.Message}"); + try + { + _cts?.Cancel(); + _cts?.Dispose(); + _udpClient?.Close(); + Console.WriteLine("Stopped listening for UDP messages."); + } + catch (Exception ex) + { + Console.WriteLine($"Error while stopping: {ex.Message}"); + } } - } - public void Exit() - { - try - { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) + public void Exit() { - Console.WriteLine($"Error while stopping: {ex.Message}"); + try + { + _cts?.Cancel(); + _udpClient?.Close(); + Console.WriteLine("Stopped listening for UDP messages."); + } + catch (Exception ex) + { + Console.WriteLine($"Error while stopping: {ex.Message}"); + } } - } - public override int GetHashCode() - { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; + 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)); + using var md5 = MD5.Create(); + var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload)); - return BitConverter.ToInt32(hash, 0); + return BitConverter.ToInt32(hash, 0); + } } -} \ No newline at end of file +} From 2aafcda8790fa049bc0bdec4d66293593b316402 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:24:12 +0300 Subject: [PATCH 08/27] fix: provide message in ArgumentOutOfRangeException (S3928) --- NetSdrClientApp/Messages/NetSdrMessageHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 43ee883..f13960b 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -105,7 +105,7 @@ public static IEnumerable GetSamples(ushort sampleSize, byte[] body) sampleSize /= 8; //to bytes if (sampleSize > 4) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(sampleSize), sampleSize, "Sample size must be 8, 16, 24, or 32 bits."); } var bodyEnumerable = body as IEnumerable; From b972a36258c4ae0f7a82393422a6d571dd318944 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:24:28 +0300 Subject: [PATCH 09/27] fix: use static MD5.HashData instead of ComputeHash (CA1850) --- NetSdrClientApp/Networking/UdpClientWrapper.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 941a33a..be691a0 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -77,8 +77,7 @@ 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)); + var hash = MD5.HashData(Encoding.UTF8.GetBytes(payload)); return BitConverter.ToInt32(hash, 0); } From 4634d91465069786ad79dae6fc8154f3a06b376f Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:38:14 +0300 Subject: [PATCH 10/27] chore: add coverlet.msbuild for OpenCover report generation --- NetSdrClientAppTests/NetSdrClientAppTests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46a..eb99533 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -11,6 +11,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + From 726d635e925494b031e2b9c4b0242077086f48fb Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:38:18 +0300 Subject: [PATCH 11/27] ci: enable test coverage step in SonarCloud workflow --- .github/workflows/sonarcloud.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 73fa9dc..49217c6 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -72,13 +72,13 @@ jobs: run: dotnet restore NetSdrClient.sln - name: Build run: dotnet build NetSdrClient.sln -c Release --no-restore - #- name: Tests with coverage (OpenCover) - # run: | - # dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` - # /p:CollectCoverage=true ` - # /p:CoverletOutput=TestResults/coverage.xml ` - # /p:CoverletOutputFormat=opencover - # shell: pwsh + - name: Tests with coverage (OpenCover) + run: | + dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` + /p:CollectCoverage=true ` + /p:CoverletOutput=TestResults/coverage.xml ` + /p:CoverletOutputFormat=opencover + shell: pwsh # 3) END: SonarScanner - name: SonarScanner End run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" From 064336d741d0dd50ab394f963f4ae003b30a3e60 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:38:22 +0300 Subject: [PATCH 12/27] fix: cast ushort to int before Enum.IsDefined to match enum underlying type --- NetSdrClientApp/Messages/NetSdrMessageHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 43ee883..b1b3469 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -77,7 +77,7 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt msgEnumarable = msgEnumarable.Skip(_msgControlItemLength); msgLength -= _msgControlItemLength; - if (Enum.IsDefined(typeof(ControlItemCodes), value)) + if (Enum.IsDefined(typeof(ControlItemCodes), (int)value)) { itemCode = (ControlItemCodes)value; } From f726d15f6b308b3a26044ed8d8306b95983c4cc5 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:38:26 +0300 Subject: [PATCH 13/27] test: add 5 unit tests for NetSdrClient and NetSdrMessageHelper --- NetSdrClientAppTests/NetSdrClientTests.cs | 24 +++++++++++ .../NetSdrMessageHelperTests.cs | 40 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f..b52e77e 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -115,5 +115,29 @@ public async Task StopIQTest() Assert.That(_client.IQStarted, Is.False); } + [Test] + public async Task StopIQNoConnectionTest() + { + // act — no ConnectAsync, so Connected is false + await _client.StopIQAsync(); + + // assert + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + _tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce); + } + + [Test] + public async Task ChangeFrequencyAsyncTest() + { + // arrange + await ConnectAsyncTest(); // 3 setup messages + + // act + await _client.ChangeFrequencyAsync(20_000_000, 1); + + // assert — 4th SendMessageAsync for frequency change + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); + } + //TODO: cover the rest of the NetSdrClient code here } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index b40fff7..285b694 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -64,6 +64,46 @@ public void GetDataItemMessageTest() Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); } + [Test] + public void TranslateMessageRoundTripTest() + { + // arrange — build a SetControlItem message then parse it back + var parameters = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + var msg = NetSdrMessageHelper.GetControlItemMessage( + NetSdrMessageHelper.MsgTypes.SetControlItem, + NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency, + parameters); + + // act + bool success = NetSdrMessageHelper.TranslateMessage(msg, + out var type, out var itemCode, out _, out var body); + + // assert + Assert.That(success, Is.True); + Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.SetControlItem)); + Assert.That(itemCode, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency)); + Assert.That(body, Is.EqualTo(parameters)); + } + + [Test] + public void GetSamples16BitReturnsCorrectCountTest() + { + // 6 bytes → 3 samples of 16 bits + var body = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + + var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); + + Assert.That(samples.Count, Is.EqualTo(3)); + } + + [Test] + public void GetSamplesThrowsForOversizedBitsTest() + { + // 40 bits / 8 = 5 bytes > 4 → ArgumentOutOfRangeException + Assert.Throws(() => + NetSdrMessageHelper.GetSamples(40, new byte[8]).ToList()); + } + //TODO: add more NetSdrMessageHelper tests } } \ No newline at end of file From 4431a31f174098890406b615e3e0ade005fbb0a5 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:48:34 +0300 Subject: [PATCH 14/27] refactor: replace duplicate Exit body with StopListening delegation --- NetSdrClientApp/Networking/UdpClientWrapper.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index be691a0..8201c46 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -59,19 +59,7 @@ public void StopListening() } } - public void Exit() - { - try - { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) - { - Console.WriteLine($"Error while stopping: {ex.Message}"); - } - } + public void Exit() => StopListening(); public override int GetHashCode() { From 5471cbf0266e2d006c51b43d66da40f3893fd21a Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:48:55 +0300 Subject: [PATCH 15/27] refactor: extract SendBytesAsync to remove duplicated stream write logic --- NetSdrClientApp/Networking/TcpClientWrapper.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 7500897..ade0102 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -65,22 +65,12 @@ public void Disconnect() } } - public async Task SendMessageAsync(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); - } - else - { - throw new InvalidOperationException("Not connected to a server."); - } - } + public Task SendMessageAsync(byte[] data) => SendBytesAsync(data); + + public Task SendMessageAsync(string str) => SendBytesAsync(Encoding.UTF8.GetBytes(str)); - public async Task SendMessageAsync(string str) + private async Task SendBytesAsync(byte[] data) { - var data = Encoding.UTF8.GetBytes(str); if (Connected && _stream != null && _stream.CanWrite) { Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); From 13ff71fa62926fd2caff09177fc262f7a1efb174 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:59:08 +0300 Subject: [PATCH 16/27] =?UTF-8?q?test:=20add=20architecture=20rules=20(Net?= =?UTF-8?q?ArchTest)=20=E2=80=94=20RED:=20Messages=20depends=20on=20Networ?= =?UTF-8?q?king?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NetSdrClientApp/Messages/MessageDispatcher.cs | 16 +++++ NetSdrClientAppTests/ArchitectureTests.cs | 69 +++++++++++++++++++ .../NetSdrClientAppTests.csproj | 1 + 3 files changed, 86 insertions(+) create mode 100644 NetSdrClientApp/Messages/MessageDispatcher.cs create mode 100644 NetSdrClientAppTests/ArchitectureTests.cs diff --git a/NetSdrClientApp/Messages/MessageDispatcher.cs b/NetSdrClientApp/Messages/MessageDispatcher.cs new file mode 100644 index 0000000..9900d0a --- /dev/null +++ b/NetSdrClientApp/Messages/MessageDispatcher.cs @@ -0,0 +1,16 @@ +using NetSdrClientApp.Networking; + +namespace NetSdrClientApp.Messages +{ + // VIOLATION: Messages layer must not depend on Networking layer. + // This class will be removed in the fix commit. + internal class MessageDispatcher + { + private readonly ITcpClient _tcpClient; + + public MessageDispatcher(ITcpClient tcpClient) + { + _tcpClient = tcpClient; + } + } +} diff --git a/NetSdrClientAppTests/ArchitectureTests.cs b/NetSdrClientAppTests/ArchitectureTests.cs new file mode 100644 index 0000000..9317b93 --- /dev/null +++ b/NetSdrClientAppTests/ArchitectureTests.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using NetArchTest.Rules; +using NetSdrClientApp; +using NetSdrClientApp.Messages; +using NetSdrClientApp.Networking; + +namespace NetSdrClientAppTests; + +public class ArchitectureTests +{ + private static readonly Assembly AppAssembly = typeof(NetSdrClient).Assembly; + + // Rule 1: Messages layer must not reference Networking layer. + // Messages encode the NetSDR protocol and should be transport-agnostic. + [Test] + public void Messages_Should_Not_Depend_On_Networking() + { + var result = Types.InAssembly(AppAssembly) + .That().ResideInNamespace("NetSdrClientApp.Messages") + .ShouldNot().HaveDependencyOn("NetSdrClientApp.Networking") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + + // Rule 2: Networking layer must not reference Messages layer. + // TcpClientWrapper / UdpClientWrapper are generic transports — they must + // remain unaware of the NetSDR message format. + [Test] + public void Networking_Should_Not_Depend_On_Messages() + { + var result = Types.InAssembly(AppAssembly) + .That().ResideInNamespace("NetSdrClientApp.Networking") + .ShouldNot().HaveDependencyOn("NetSdrClientApp.Messages") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + + // Rule 3: All interfaces in the Networking namespace must start with 'I'. + [Test] + public void Networking_Interfaces_Should_Start_With_I() + { + var result = Types.InAssembly(AppAssembly) + .That().ResideInNamespace("NetSdrClientApp.Networking") + .And().AreInterfaces() + .Should().HaveNameStartingWith("I") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + + // Rule 4: All types in the Networking namespace must be public. + // They are part of the public contract injected into NetSdrClient. + [Test] + public void Networking_Types_Should_Be_Public() + { + var result = Types.InAssembly(AppAssembly) + .That().ResideInNamespace("NetSdrClientApp.Networking") + .Should().BePublic() + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } +} diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index eb99533..728eebf 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -17,6 +17,7 @@ + From 7d8aba5cd21a39bb1977b8618b204937345f2af1 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 12:59:12 +0300 Subject: [PATCH 17/27] =?UTF-8?q?fix:=20remove=20MessageDispatcher=20that?= =?UTF-8?q?=20violated=20Messages=E2=86=92Networking=20dependency=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NetSdrClientApp/Messages/MessageDispatcher.cs | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 NetSdrClientApp/Messages/MessageDispatcher.cs diff --git a/NetSdrClientApp/Messages/MessageDispatcher.cs b/NetSdrClientApp/Messages/MessageDispatcher.cs deleted file mode 100644 index 9900d0a..0000000 --- a/NetSdrClientApp/Messages/MessageDispatcher.cs +++ /dev/null @@ -1,16 +0,0 @@ -using NetSdrClientApp.Networking; - -namespace NetSdrClientApp.Messages -{ - // VIOLATION: Messages layer must not depend on Networking layer. - // This class will be removed in the fix commit. - internal class MessageDispatcher - { - private readonly ITcpClient _tcpClient; - - public MessageDispatcher(ITcpClient tcpClient) - { - _tcpClient = tcpClient; - } - } -} From c413acbe2673aa7d6c70ba7d834dc16be5949615 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 13:27:54 +0300 Subject: [PATCH 18/27] refactor: extract ITcpListener, split EchoServer into separate files for testability - Add ITcpListener interface and TcpListenerWrapper to decouple TcpListener - Extract EchoServer into EchoServer.cs with injected ITcpListener and logger - Extract UdpTimedSender into its own file; move Random to field to avoid per-call allocation - Make HandleClientAsync public and accept Stream for direct unit testing - Reduce Program.cs to entry-point only --- EchoTcpServer/EchoServer.cs | 71 +++++++ EchoTcpServer/EchoServer.csproj | 1 + EchoTcpServer/Networking/ITcpListener.cs | 8 + .../Networking/TcpListenerWrapper.cs | 23 +++ EchoTcpServer/Program.cs | 181 ++---------------- EchoTcpServer/UdpTimedSender.cs | 64 +++++++ 6 files changed, 179 insertions(+), 169 deletions(-) create mode 100644 EchoTcpServer/EchoServer.cs create mode 100644 EchoTcpServer/Networking/ITcpListener.cs create mode 100644 EchoTcpServer/Networking/TcpListenerWrapper.cs create mode 100644 EchoTcpServer/UdpTimedSender.cs diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs new file mode 100644 index 0000000..e1288f5 --- /dev/null +++ b/EchoTcpServer/EchoServer.cs @@ -0,0 +1,71 @@ +using EchoTcpServer.Networking; + +namespace EchoTcpServer; + +public class EchoServer +{ + private readonly ITcpListener _listener; + private readonly Action _log; + private CancellationTokenSource _cts = new(); + + public EchoServer(ITcpListener listener, Action? log = null) + { + _listener = listener; + _log = log ?? Console.WriteLine; + } + + public async Task StartAsync() + { + _listener.Start(); + _log("Server started."); + + while (!_cts.Token.IsCancellationRequested) + { + try + { + var stream = await _listener.AcceptClientStreamAsync(); + _log("Client connected."); + _ = Task.Run(() => HandleClientAsync(stream, _cts.Token)); + } + catch (ObjectDisposedException) + { + break; + } + } + + _log("Server shutdown."); + } + + public async Task HandleClientAsync(Stream stream, CancellationToken token) + { + using (stream) + { + try + { + byte[] buffer = new byte[8192]; + int bytesRead; + + while (!token.IsCancellationRequested + && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + { + await stream.WriteAsync(buffer, 0, bytesRead, token); + _log($"Echoed {bytesRead} bytes."); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _log($"Error: {ex.Message}"); + } + } + + _log("Client disconnected."); + } + + public void Stop() + { + _cts.Cancel(); + _listener.Stop(); + _cts.Dispose(); + _log("Server stopped."); + } +} diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTcpServer/EchoServer.csproj index 2150e37..46e738e 100644 --- a/EchoTcpServer/EchoServer.csproj +++ b/EchoTcpServer/EchoServer.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + EchoTcpServer diff --git a/EchoTcpServer/Networking/ITcpListener.cs b/EchoTcpServer/Networking/ITcpListener.cs new file mode 100644 index 0000000..9952cc0 --- /dev/null +++ b/EchoTcpServer/Networking/ITcpListener.cs @@ -0,0 +1,8 @@ +namespace EchoTcpServer.Networking; + +public interface ITcpListener +{ + void Start(); + void Stop(); + Task AcceptClientStreamAsync(); +} diff --git a/EchoTcpServer/Networking/TcpListenerWrapper.cs b/EchoTcpServer/Networking/TcpListenerWrapper.cs new file mode 100644 index 0000000..67b84a7 --- /dev/null +++ b/EchoTcpServer/Networking/TcpListenerWrapper.cs @@ -0,0 +1,23 @@ +using System.Net; +using System.Net.Sockets; + +namespace EchoTcpServer.Networking; + +public class TcpListenerWrapper : ITcpListener +{ + private readonly TcpListener _listener; + + public TcpListenerWrapper(int port) + { + _listener = new TcpListener(IPAddress.Any, port); + } + + public void Start() => _listener.Start(); + public void Stop() => _listener.Stop(); + + public async Task AcceptClientStreamAsync() + { + var client = await _listener.AcceptTcpClientAsync(); + return client.GetStream(); + } +} diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c57..ab9890f 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,173 +1,16 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using EchoTcpServer; +using EchoTcpServer.Networking; -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer -{ - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; +var server = new EchoServer(new TcpListenerWrapper(5000)); +_ = Task.Run(() => server.StartAsync()); +using var sender = new UdpTimedSender("127.0.0.1", 60000); +Console.WriteLine("Press any key to stop sending..."); +sender.StartSending(5000); - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } +Console.WriteLine("Press 'q' to quit..."); +while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { } - 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); - - // Start the server in a separate task - _ = Task.Run(() => server.StartAsync()); - - string host = "127.0.0.1"; // Target IP - int port = 60000; // Target Port - int intervalMilliseconds = 5000; // Send every 3 seconds - - using (var sender = new UdpTimedSender(host, port)) - { - Console.WriteLine("Press any key to stop sending..."); - sender.StartSending(intervalMilliseconds); - - Console.WriteLine("Press 'q' to quit..."); - while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) - { - // Just wait until 'q' is pressed - } - - sender.StopSending(); - server.Stop(); - 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 +sender.StopSending(); +server.Stop(); +Console.WriteLine("Sender stopped."); diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs new file mode 100644 index 0000000..aabf3a6 --- /dev/null +++ b/EchoTcpServer/UdpTimedSender.cs @@ -0,0 +1,64 @@ +using System.Net; +using System.Net.Sockets; + +namespace EchoTcpServer; + +public class UdpTimedSender : IDisposable +{ + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private readonly Random _rnd = new(); + private Timer? _timer; + private ushort _sequenceNumber; + + 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); + } + + private void SendMessageCallback(object? state) + { + try + { + byte[] samples = new byte[1024]; + _rnd.NextBytes(samples); + _sequenceNumber++; + + byte[] msg = new byte[] { 0x04, 0x84 } + .Concat(BitConverter.GetBytes(_sequenceNumber)) + .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(); + } +} From 7b5dea351871259318fc3423e17718ae5bc2a019 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 13:28:00 +0300 Subject: [PATCH 19/27] test: add EchoTcpServerTests project with 13 unit tests for EchoServer and UdpTimedSender - EchoServerTests: StartAsync lifecycle, HandleClientAsync echo logic, cancellation, stream exception handling, client connect/disconnect logging - UdpTimedSenderTests: double-start guard, stop without start, restart after stop, dispose lifecycle --- EchoTcpServerTests/EchoServerTests.cs | 159 +++++++++++++++++++ EchoTcpServerTests/EchoTcpServerTests.csproj | 28 ++++ EchoTcpServerTests/UdpTimedSenderTests.cs | 51 ++++++ NetSdrClient.sln | 44 +++++ 4 files changed, 282 insertions(+) create mode 100644 EchoTcpServerTests/EchoServerTests.cs create mode 100644 EchoTcpServerTests/EchoTcpServerTests.csproj create mode 100644 EchoTcpServerTests/UdpTimedSenderTests.cs diff --git a/EchoTcpServerTests/EchoServerTests.cs b/EchoTcpServerTests/EchoServerTests.cs new file mode 100644 index 0000000..b81c47f --- /dev/null +++ b/EchoTcpServerTests/EchoServerTests.cs @@ -0,0 +1,159 @@ +using Moq; +using EchoTcpServer; +using EchoTcpServer.Networking; + +namespace EchoTcpServerTests; + +[TestFixture] +public class EchoServerTests +{ + private Mock _listenerMock = null!; + private List _logs = null!; + private EchoServer _server = null!; + + [SetUp] + public void SetUp() + { + _listenerMock = new Mock(); + _logs = new List(); + _server = new EchoServer(_listenerMock.Object, msg => _logs.Add(msg)); + } + + [Test] + public void Stop_StopsListenerAndLogs() + { + _server.Stop(); + + _listenerMock.Verify(l => l.Stop(), Times.Once); + Assert.That(_logs, Contains.Item("Server stopped.")); + } + + [Test] + public async Task StartAsync_StartsListenerAndLogsShutdown() + { + _listenerMock + .Setup(l => l.AcceptClientStreamAsync()) + .ThrowsAsync(new ObjectDisposedException("listener")); + + await _server.StartAsync(); + + _listenerMock.Verify(l => l.Start(), Times.Once); + Assert.That(_logs, Contains.Item("Server started.")); + Assert.That(_logs, Contains.Item("Server shutdown.")); + } + + [Test] + public async Task StartAsync_AcceptsClientAndLogs() + { + _listenerMock.SetupSequence(l => l.AcceptClientStreamAsync()) + .ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })) + .ThrowsAsync(new ObjectDisposedException("listener")); + + await _server.StartAsync(); + + _listenerMock.Verify(l => l.AcceptClientStreamAsync(), Times.Exactly(2)); + Assert.That(_logs, Contains.Item("Client connected.")); + } + + [Test] + public async Task HandleClientAsync_EchoesInputToOutput() + { + var inputData = new byte[] { 0x41, 0x42, 0x43 }; // "ABC" + var inputStream = new MemoryStream(inputData); + var outputStream = new MemoryStream(); + var stream = new DuplexStream(inputStream, outputStream); + + await _server.HandleClientAsync(stream, CancellationToken.None); + + Assert.That(outputStream.ToArray(), Is.EqualTo(inputData)); + } + + [Test] + public async Task HandleClientAsync_LogsEchoedBytes() + { + var stream = new DuplexStream(new MemoryStream(new byte[] { 1, 2 }), new MemoryStream()); + + await _server.HandleClientAsync(stream, CancellationToken.None); + + Assert.That(_logs.Any(m => m.StartsWith("Echoed")), Is.True); + Assert.That(_logs, Contains.Item("Client disconnected.")); + } + + [Test] + public async Task HandleClientAsync_PreCancelledToken_DoesNotRead() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var mockStream = new Mock(); + mockStream.Setup(s => s.CanRead).Returns(true); + + await _server.HandleClientAsync(mockStream.Object, cts.Token); + + mockStream.Verify( + s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleClientAsync_StreamException_LogsError() + { + var mockStream = new Mock(); + mockStream + .Setup(s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new IOException("Connection reset by peer")); + + await _server.HandleClientAsync(mockStream.Object, CancellationToken.None); + + Assert.That(_logs.Any(m => m.StartsWith("Error:")), Is.True); + } + + [Test] + public async Task HandleClientAsync_EmptyStream_LogsDisconnected() + { + var emptyStream = new DuplexStream(new MemoryStream(Array.Empty()), new MemoryStream()); + + await _server.HandleClientAsync(emptyStream, CancellationToken.None); + + Assert.That(_logs, Contains.Item("Client disconnected.")); + } +} + +/// +/// Reads from one stream, writes to another — simulates a bidirectional client connection. +/// +internal sealed class DuplexStream : Stream +{ + private readonly Stream _read; + private readonly Stream _write; + + public DuplexStream(Stream read, Stream write) + { + _read = read; + _write = write; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _write.Flush(); + public override int Read(byte[] buffer, int offset, int count) => _read.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _read.ReadAsync(buffer, offset, count, ct); + + public override void Write(byte[] buffer, int offset, int count) => _write.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _write.WriteAsync(buffer, offset, count, ct); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); +} diff --git a/EchoTcpServerTests/EchoTcpServerTests.csproj b/EchoTcpServerTests/EchoTcpServerTests.csproj new file mode 100644 index 0000000..b46e32b --- /dev/null +++ b/EchoTcpServerTests/EchoTcpServerTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/EchoTcpServerTests/UdpTimedSenderTests.cs b/EchoTcpServerTests/UdpTimedSenderTests.cs new file mode 100644 index 0000000..616b8b7 --- /dev/null +++ b/EchoTcpServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,51 @@ +using EchoTcpServer; + +namespace EchoTcpServerTests; + +[TestFixture] +public class UdpTimedSenderTests +{ + [Test] + public void StartSending_WhenAlreadyRunning_ThrowsInvalidOperationException() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.Throws(() => sender.StartSending(60_000)); + } + + [Test] + public void StopSending_WithoutStarting_DoesNotThrow() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.StopSending()); + } + + [Test] + public void StopSending_AfterStart_AllowsSecondStart() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + sender.StopSending(); + + Assert.DoesNotThrow(() => sender.StartSending(60_000)); + } + + [Test] + public void Dispose_WithoutStarting_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.Dispose()); + } + + [Test] + public void Dispose_WhileSending_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.DoesNotThrow(() => sender.Dispose()); + } +} diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb..959be4e 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,24 +9,68 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "Net EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoTcpServerTests", "EchoTcpServerTests\EchoTcpServerTests.csproj", "{E6BC0BE9-A629-48C5-A08A-8373A21434F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EchoTcpServer", "EchoTcpServer", "{DBDFFC83-AD19-3622-0FF4-C4288E16DE63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x64.Build.0 = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x86.Build.0 = Debug|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|Any CPU.Build.0 = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x64.ActiveCfg = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x64.Build.0 = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x86.ActiveCfg = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x86.Build.0 = Release|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x64.Build.0 = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x86.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 + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x64.ActiveCfg = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x64.Build.0 = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x86.ActiveCfg = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x64.Build.0 = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x86.ActiveCfg = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x86.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 + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x64.ActiveCfg = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x64.Build.0 = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x86.ActiveCfg = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x86.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x64.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x86.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|Any CPU.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x64.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x64.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x86.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 3649d64e9e6c84fce9a83a99dca884c31e5af5ae Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 13:57:28 +0300 Subject: [PATCH 20/27] fix: replace MD5 GetHashCode with HashCode.Combine, add Equals; fix null-forgiving operator on _cts --- NetSdrClientApp/Networking/TcpClientWrapper.cs | 4 ++-- NetSdrClientApp/Networking/UdpClientWrapper.cs | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index ade0102..e4b4b03 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -90,7 +90,7 @@ private async Task StartListeningAsync() { Console.WriteLine($"Starting listening for incomming messages."); - while (!_cts.Token.IsCancellationRequested) + while (!_cts!.Token.IsCancellationRequested) { byte[] buffer = new byte[8194]; @@ -103,7 +103,7 @@ private async Task StartListeningAsync() } catch (OperationCanceledException) { - //empty + // cancellation requested — no action needed } catch (Exception ex) { diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 8201c46..90e371e 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,7 +1,5 @@ using System.Net; using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; namespace NetSdrClientApp.Networking { @@ -36,7 +34,7 @@ public async Task StartListeningAsync() } catch (OperationCanceledException) { - //empty + // cancellation requested — no action needed } catch (Exception ex) { @@ -61,13 +59,11 @@ public void StopListening() public void Exit() => StopListening(); - public override int GetHashCode() - { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; - - var hash = MD5.HashData(Encoding.UTF8.GetBytes(payload)); + public override int GetHashCode() => + HashCode.Combine(_localEndPoint.Address, _localEndPoint.Port); - return BitConverter.ToInt32(hash, 0); - } + public override bool Equals(object? obj) => + obj is UdpClientWrapper other && + _localEndPoint.Equals(other._localEndPoint); } } From afb1f69acb9410b463bbb2f8ad7038e6f970ddeb Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 13:57:35 +0300 Subject: [PATCH 21/27] refactor: remove TODO comment, simplify GetMessage with collection expression, replace O(n) Count() loop in GetSamples with index-based iteration --- .../Messages/NetSdrMessageHelper.cs | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index b0ec931..9d64e8d 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -1,7 +1,6 @@  namespace NetSdrClientApp.Messages { - //TODO: analyze possible use of [StructLayout] for better performance and readability public static class NetSdrMessageHelper { private const short _maxMessageLength = 8191; @@ -44,20 +43,13 @@ public static byte[] GetDataItemMessage(MsgTypes type, byte[] parameters) private static byte[] GetMessage(MsgTypes type, ControlItemCodes itemCode, byte[] parameters) { - var itemCodeBytes = Array.Empty(); - if (itemCode != ControlItemCodes.None) - { - itemCodeBytes = BitConverter.GetBytes((ushort)itemCode); - } + var itemCodeBytes = itemCode != ControlItemCodes.None + ? BitConverter.GetBytes((ushort)itemCode) + : Array.Empty(); var headerBytes = GetHeader(type, itemCodeBytes.Length + parameters.Length); - List msg = new List(); - msg.AddRange(headerBytes); - msg.AddRange(itemCodeBytes); - msg.AddRange(parameters); - - return msg.ToArray(); + return [.. headerBytes, .. itemCodeBytes, .. parameters]; } public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlItemCodes itemCode, out ushort sequenceNumber, out byte[] body) @@ -102,23 +94,16 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt public static IEnumerable GetSamples(ushort sampleSize, byte[] body) { - sampleSize /= 8; //to bytes - if (sampleSize > 4) - { + int bytesPerSample = sampleSize / 8; + if (bytesPerSample > 4) throw new ArgumentOutOfRangeException(nameof(sampleSize), sampleSize, "Sample size must be 8, 16, 24, or 32 bits."); - } - - var bodyEnumerable = body as IEnumerable; - var prefixBytes = Enumerable.Range(0, 4 - sampleSize) - .Select(b => (byte)0); - while (bodyEnumerable.Count() >= sampleSize) + var buffer = new byte[4]; + for (int offset = 0; offset + bytesPerSample <= body.Length; offset += bytesPerSample) { - yield return BitConverter.ToInt32(bodyEnumerable - .Take(sampleSize) - .Concat(prefixBytes) - .ToArray()); - bodyEnumerable = bodyEnumerable.Skip(sampleSize); + Array.Clear(buffer, 0, 4); + Array.Copy(body, offset, buffer, 0, bytesPerSample); + yield return BitConverter.ToInt32(buffer, 0); } } From ce5da40d8e5c51fd92b396f869ba1727cd1c7464 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 13:57:40 +0300 Subject: [PATCH 22/27] ci: add EchoTcpServerTests coverage to SonarCloud analysis --- .github/workflows/sonarcloud.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 49217c6..d5d548f 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -78,6 +78,10 @@ jobs: /p:CollectCoverage=true ` /p:CoverletOutput=TestResults/coverage.xml ` /p:CoverletOutputFormat=opencover + dotnet test EchoTcpServerTests/EchoTcpServerTests.csproj -c Release --no-build ` + /p:CollectCoverage=true ` + /p:CoverletOutput=TestResults/coverage.xml ` + /p:CoverletOutputFormat=opencover shell: pwsh # 3) END: SonarScanner - name: SonarScanner End From c9edb8354b40de08bd3fbdf28e6d9b2afb400bb5 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 13:57:44 +0300 Subject: [PATCH 23/27] test: add UdpClientWrapper equality/hash tests; cover UDP event handler and double-connect guard --- NetSdrClientAppTests/NetSdrClientTests.cs | 34 +++++++++++- NetSdrClientAppTests/UdpClientWrapperTests.cs | 52 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 NetSdrClientAppTests/UdpClientWrapperTests.cs diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index b52e77e..2704500 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -1,5 +1,6 @@ using Moq; using NetSdrClientApp; +using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; namespace NetSdrClientAppTests; @@ -139,5 +140,36 @@ public async Task ChangeFrequencyAsyncTest() _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); } - //TODO: cover the rest of the NetSdrClient code here + [Test] + public async Task UdpMessageReceived_ValidDataItem_DoesNotThrow() + { + await ConnectAsyncTest(); + await _client.StartIQAsync(); + + // build a valid DataItem2 payload: 2-byte sequence number + sample bytes + var seqNum = BitConverter.GetBytes((ushort)1); + var sampleData = new byte[] { 0x0A, 0x0B, 0x0C, 0x0D }; // 2 × 16-bit samples + var payload = seqNum.Concat(sampleData).ToArray(); + var msg = NetSdrMessageHelper.GetDataItemMessage(NetSdrMessageHelper.MsgTypes.DataItem2, payload); + + Assert.DoesNotThrow(() => + _updMock.Raise(udp => udp.MessageReceived += null, _updMock.Object, msg)); + + // cleanup artefact created by the handler + if (File.Exists("samples.bin")) + File.Delete("samples.bin"); + } + + [Test] + public async Task ConnectAsync_AlreadyConnected_DoesNotReconnect() + { + await ConnectAsyncTest(); // connects and sends 3 setup messages + + // act — call ConnectAsync again while already connected + await _client.ConnectAsync(); + + // assert — still only 3 messages (no second setup) + _tcpMock.Verify(tcp => tcp.Connect(), Times.Once); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(3)); + } } diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs new file mode 100644 index 0000000..9fd10c9 --- /dev/null +++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs @@ -0,0 +1,52 @@ +using NetSdrClientApp.Networking; + +namespace NetSdrClientAppTests; + +[TestFixture] +public class UdpClientWrapperTests +{ + [Test] + public void GetHashCode_IsConsistentAcrossCalls() + { + var wrapper = new UdpClientWrapper(12399); + Assert.That(wrapper.GetHashCode(), Is.EqualTo(wrapper.GetHashCode())); + } + + [Test] + public void GetHashCode_DifferentPorts_ReturnDifferentValues() + { + var w1 = new UdpClientWrapper(12399); + var w2 = new UdpClientWrapper(12400); + Assert.That(w1.GetHashCode(), Is.Not.EqualTo(w2.GetHashCode())); + } + + [Test] + public void Equals_SamePort_ReturnsTrue() + { + var w1 = new UdpClientWrapper(12399); + var w2 = new UdpClientWrapper(12399); + Assert.That(w1.Equals(w2), Is.True); + } + + [Test] + public void Equals_DifferentPort_ReturnsFalse() + { + var w1 = new UdpClientWrapper(12399); + var w2 = new UdpClientWrapper(12400); + Assert.That(w1.Equals(w2), Is.False); + } + + [Test] + public void Equals_Null_ReturnsFalse() + { + var wrapper = new UdpClientWrapper(12399); + Assert.That(wrapper.Equals(null), Is.False); + } + + [Test] + public void Equals_DifferentType_ReturnsFalse() + { + var wrapper = new UdpClientWrapper(12399); + Assert.That(wrapper.Equals("not a wrapper"), Is.False); + } +} From 3fdd60e4c6d08692afeb8732fcebaf4c538e5a0f Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 14:07:03 +0300 Subject: [PATCH 24/27] fix: resolve SonarCloud code smells and hotspots for Quality Gate - Wrap independent asserts in Assert.Multiple across test files - Replace Has.Count / .Count() on IEnumerable with Has.Exactly(n).Items - Switch ReadAsync/WriteAsync to Memory-based overloads (S4583) - Make _cts readonly in TcpClientWrapper; fix null-forgiving operator - Split GetSamples into guard + private iterator (S3972) - Fix IDisposable pattern in UdpTimedSender with Dispose(bool) - Make _udpClient_MessageReceived static (S2325) - Remove TODO comment; fix empty while block in Program.cs - Add ErrorStream helper replacing Mock for exception tests --- EchoTcpServer/EchoServer.cs | 6 +- EchoTcpServer/Program.cs | 5 +- EchoTcpServer/UdpTimedSender.cs | 13 +++- EchoTcpServerTests/EchoServerTests.cs | 64 ++++++++++++++----- .../Messages/NetSdrMessageHelper.cs | 5 ++ NetSdrClientApp/NetSdrClient.cs | 3 +- .../Networking/TcpClientWrapper.cs | 4 +- .../NetSdrMessageHelperTests.cs | 41 ++++++------ 8 files changed, 96 insertions(+), 45 deletions(-) diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index e1288f5..2a36f0d 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -6,7 +6,7 @@ public class EchoServer { private readonly ITcpListener _listener; private readonly Action _log; - private CancellationTokenSource _cts = new(); + private readonly CancellationTokenSource _cts = new(); public EchoServer(ITcpListener listener, Action? log = null) { @@ -46,9 +46,9 @@ public async Task HandleClientAsync(Stream stream, CancellationToken token) int bytesRead; while (!token.IsCancellationRequested - && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + && (bytesRead = await stream.ReadAsync(buffer.AsMemory(), token)) > 0) { - await stream.WriteAsync(buffer, 0, bytesRead, token); + await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); _log($"Echoed {bytesRead} bytes."); } } diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index ab9890f..bfa9b2a 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -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(); diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs index aabf3a6..ef9d7a2 100644 --- a/EchoTcpServer/UdpTimedSender.cs +++ b/EchoTcpServer/UdpTimedSender.cs @@ -58,7 +58,16 @@ public void StopSending() public void Dispose() { - StopSending(); - _udpClient.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + StopSending(); + _udpClient.Dispose(); + } } } diff --git a/EchoTcpServerTests/EchoServerTests.cs b/EchoTcpServerTests/EchoServerTests.cs index b81c47f..19e9ac9 100644 --- a/EchoTcpServerTests/EchoServerTests.cs +++ b/EchoTcpServerTests/EchoServerTests.cs @@ -38,8 +38,11 @@ public async Task StartAsync_StartsListenerAndLogsShutdown() await _server.StartAsync(); _listenerMock.Verify(l => l.Start(), Times.Once); - Assert.That(_logs, Contains.Item("Server started.")); - Assert.That(_logs, Contains.Item("Server shutdown.")); + Assert.Multiple(() => + { + Assert.That(_logs, Contains.Item("Server started.")); + Assert.That(_logs, Contains.Item("Server shutdown.")); + }); } [Test] @@ -75,8 +78,11 @@ public async Task HandleClientAsync_LogsEchoedBytes() await _server.HandleClientAsync(stream, CancellationToken.None); - Assert.That(_logs.Any(m => m.StartsWith("Echoed")), Is.True); - Assert.That(_logs, Contains.Item("Client disconnected.")); + Assert.Multiple(() => + { + Assert.That(_logs.Any(m => m.StartsWith("Echoed")), Is.True); + Assert.That(_logs, Contains.Item("Client disconnected.")); + }); } [Test] @@ -85,25 +91,16 @@ public async Task HandleClientAsync_PreCancelledToken_DoesNotRead() var cts = new CancellationTokenSource(); cts.Cancel(); - var mockStream = new Mock(); - mockStream.Setup(s => s.CanRead).Returns(true); - - await _server.HandleClientAsync(mockStream.Object, cts.Token); + var stream = new DuplexStream(new MemoryStream(), new MemoryStream()); + await _server.HandleClientAsync(stream, cts.Token); - mockStream.Verify( - s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); + Assert.That(_logs, Contains.Item("Client disconnected.")); } [Test] public async Task HandleClientAsync_StreamException_LogsError() { - var mockStream = new Mock(); - mockStream - .Setup(s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new IOException("Connection reset by peer")); - - await _server.HandleClientAsync(mockStream.Object, CancellationToken.None); + await _server.HandleClientAsync(new ErrorStream(), CancellationToken.None); Assert.That(_logs.Any(m => m.StartsWith("Error:")), Is.True); } @@ -149,11 +146,44 @@ public override long Position public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) => _read.ReadAsync(buffer, offset, count, ct); + public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) + => _read.ReadAsync(buffer, ct); + public override void Write(byte[] buffer, int offset, int count) => _write.Write(buffer, offset, count); public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) => _write.WriteAsync(buffer, offset, count, ct); + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ct = default) + => _write.WriteAsync(buffer, ct); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); +} + +/// +/// Stream that throws IOException on every read — simulates a broken connection. +/// +internal sealed class ErrorStream : Stream +{ + public override bool CanRead => true; + public override bool CanWrite => true; + public override bool CanSeek => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() { } + + public override int Read(byte[] buffer, int offset, int count) => + throw new IOException("Connection reset by peer"); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) => + ValueTask.FromException(new IOException("Connection reset by peer")); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) => + Task.FromException(new IOException("Connection reset by peer")); + + public override void Write(byte[] buffer, int offset, int count) { } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); } diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 9d64e8d..5708f8f 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -98,6 +98,11 @@ public static IEnumerable GetSamples(ushort sampleSize, byte[] body) if (bytesPerSample > 4) throw new ArgumentOutOfRangeException(nameof(sampleSize), sampleSize, "Sample size must be 8, 16, 24, or 32 bits."); + return GetSamplesIterator(bytesPerSample, body); + } + + private static IEnumerable GetSamplesIterator(int bytesPerSample, byte[] body) + { var buffer = new byte[4]; for (int offset = 0; offset + bytesPerSample <= body.Length; offset += bytesPerSample) { diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index 1b9a04f..9378114 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -106,7 +106,7 @@ public async Task ChangeFrequencyAsync(long hz, int channel) await SendTcpRequest(msg); } - private void _udpClient_MessageReceived(object? sender, byte[] e) + private static void _udpClient_MessageReceived(object? sender, byte[] e) { NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body); var samples = NetSdrMessageHelper.GetSamples(16, body); @@ -145,7 +145,6 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) private void _tcpClient_MessageReceived(object? sender, byte[] e) { - //TODO: add Unsolicited messages handling here if (responseTaskSource != null) { responseTaskSource.SetResult(e); diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index e4b4b03..c005800 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -74,7 +74,7 @@ private async Task SendBytesAsync(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()); } else { @@ -94,7 +94,7 @@ private async Task StartListeningAsync() { byte[] buffer = new byte[8194]; - int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token); + int bytesRead = await _stream.ReadAsync(buffer.AsMemory(), _cts.Token); if (bytesRead > 0) { MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 285b694..a5bbf1e 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -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)); + Assert.Multiple(() => + { + Assert.That(headerBytes, Has.Exactly(2).Items); + Assert.That(msg.Length, Is.EqualTo(actualLength)); + Assert.That(type, Is.EqualTo(actualType)); + Assert.That(actualCode, Is.EqualTo((short)code)); + Assert.That(parametersBytes, Has.Exactly(parametersLength).Items); + }); } [Test] @@ -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)); + Assert.Multiple(() => + { + Assert.That(headerBytes, Has.Exactly(2).Items); + Assert.That(msg.Length, Is.EqualTo(actualLength)); + Assert.That(type, Is.EqualTo(actualType)); + Assert.That(parametersBytes, Has.Exactly(parametersLength).Items); + }); } [Test] @@ -79,10 +82,13 @@ public void TranslateMessageRoundTripTest() out var type, out var itemCode, out _, out var body); // assert - Assert.That(success, Is.True); - Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.SetControlItem)); - Assert.That(itemCode, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency)); - Assert.That(body, Is.EqualTo(parameters)); + Assert.Multiple(() => + { + Assert.That(success, Is.True); + Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.SetControlItem)); + Assert.That(itemCode, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency)); + Assert.That(body, Is.EqualTo(parameters)); + }); } [Test] @@ -93,7 +99,7 @@ public void GetSamples16BitReturnsCorrectCountTest() var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); - Assert.That(samples.Count, Is.EqualTo(3)); + Assert.That(samples, Has.Count.EqualTo(3)); } [Test] @@ -104,6 +110,5 @@ public void GetSamplesThrowsForOversizedBitsTest() NetSdrMessageHelper.GetSamples(40, new byte[8]).ToList()); } - //TODO: add more NetSdrMessageHelper tests } } \ No newline at end of file From 7e45a6c05b4ad08c001ad7c05ae41eebd753f13f Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 14:24:55 +0300 Subject: [PATCH 25/27] fix: resolve NUnit2009/2021 analyzer warnings in UdpClientWrapperTests; add coverlet.msbuild to EchoTcpServerTests - Replace same-argument GetHashCode assertion with captured variable (NUnit2009) - Use Is.EqualTo/Is.Not.EqualTo on objects directly instead of .Equals()+Is.True/False - Replace Is.Not.EqualTo("string") with new object() to avoid NUnit2021 (incompatible types) - Add coverlet.msbuild to EchoTcpServerTests.csproj so /p:CollectCoverage=true produces an OpenCover report; without it EchoTcpServer coverage was silently absent from SonarCloud --- EchoTcpServerTests/EchoTcpServerTests.csproj | 1 + NetSdrClientAppTests/UdpClientWrapperTests.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/EchoTcpServerTests/EchoTcpServerTests.csproj b/EchoTcpServerTests/EchoTcpServerTests.csproj index b46e32b..7a2ac1f 100644 --- a/EchoTcpServerTests/EchoTcpServerTests.csproj +++ b/EchoTcpServerTests/EchoTcpServerTests.csproj @@ -10,6 +10,7 @@ + diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs index 9fd10c9..c5ed078 100644 --- a/NetSdrClientAppTests/UdpClientWrapperTests.cs +++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs @@ -9,7 +9,8 @@ public class UdpClientWrapperTests public void GetHashCode_IsConsistentAcrossCalls() { var wrapper = new UdpClientWrapper(12399); - Assert.That(wrapper.GetHashCode(), Is.EqualTo(wrapper.GetHashCode())); + var hash = wrapper.GetHashCode(); + Assert.That(wrapper.GetHashCode(), Is.EqualTo(hash)); } [Test] @@ -25,7 +26,7 @@ public void Equals_SamePort_ReturnsTrue() { var w1 = new UdpClientWrapper(12399); var w2 = new UdpClientWrapper(12399); - Assert.That(w1.Equals(w2), Is.True); + Assert.That(w1, Is.EqualTo(w2)); } [Test] @@ -33,20 +34,20 @@ public void Equals_DifferentPort_ReturnsFalse() { var w1 = new UdpClientWrapper(12399); var w2 = new UdpClientWrapper(12400); - Assert.That(w1.Equals(w2), Is.False); + Assert.That(w1, Is.Not.EqualTo(w2)); } [Test] public void Equals_Null_ReturnsFalse() { var wrapper = new UdpClientWrapper(12399); - Assert.That(wrapper.Equals(null), Is.False); + Assert.That(wrapper, Is.Not.EqualTo(null)); } [Test] public void Equals_DifferentType_ReturnsFalse() { var wrapper = new UdpClientWrapper(12399); - Assert.That(wrapper.Equals("not a wrapper"), Is.False); + Assert.That(wrapper, Is.Not.EqualTo(new object())); } } From 61c57eb2ca4087c9b3cb495e1037c4e8f31d41e7 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 14:41:50 +0300 Subject: [PATCH 26/27] test: add TcpClientWrapperTests to cover Memory-based Send/Receive paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration tests spin up a local TcpListener on a random port so the wrapper's SendBytesAsync (WriteAsync(data.AsMemory())) and StartListeningAsync (ReadAsync(buffer.AsMemory(), token)) paths are exercised — the three new lines changed in lab8 that had 0% coverage. Also adds disconnect-when-not-connected and send-when-not-connected edge-case tests to cover the guard branches. --- NetSdrClientAppTests/TcpClientWrapperTests.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 NetSdrClientAppTests/TcpClientWrapperTests.cs diff --git a/NetSdrClientAppTests/TcpClientWrapperTests.cs b/NetSdrClientAppTests/TcpClientWrapperTests.cs new file mode 100644 index 0000000..11d6c54 --- /dev/null +++ b/NetSdrClientAppTests/TcpClientWrapperTests.cs @@ -0,0 +1,62 @@ +using System.Net; +using System.Net.Sockets; +using NetSdrClientApp.Networking; + +namespace NetSdrClientAppTests; + +[TestFixture] +public class TcpClientWrapperTests +{ + [Test] + public void Disconnect_WhenNotConnected_DoesNotThrow() + { + var wrapper = new TcpClientWrapper("127.0.0.1", 19399); + Assert.DoesNotThrow(() => wrapper.Disconnect()); + } + + [Test] + public async Task SendMessageAsync_WhenNotConnected_ThrowsInvalidOperationException() + { + var wrapper = new TcpClientWrapper("127.0.0.1", 19399); + Assert.ThrowsAsync( + () => wrapper.SendMessageAsync(new byte[] { 1 })); + await Task.CompletedTask; + } + + [Test] + public async Task Connect_SendMessage_ReceivesEchoViaEvent() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + try + { + var wrapper = new TcpClientWrapper("127.0.0.1", port); + var acceptTask = listener.AcceptTcpClientAsync(); + + wrapper.Connect(); + using var server = await acceptTask; + var serverStream = server.GetStream(); + + var received = new TaskCompletionSource(); + wrapper.MessageReceived += (_, data) => received.TrySetResult(data); + + var sendData = new byte[] { 0xAA, 0xBB, 0xCC }; + await wrapper.SendMessageAsync(sendData); + + var buf = new byte[sendData.Length]; + int n = await serverStream.ReadAsync(buf.AsMemory()); + await serverStream.WriteAsync(buf.AsMemory(0, n)); + + var result = await received.Task.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.That(result, Is.EqualTo(sendData)); + + wrapper.Disconnect(); + } + finally + { + listener.Stop(); + } + } +} From f3292ef0b4b899e399abd77720d93d5798eca2b6 Mon Sep 17 00:00:00 2001 From: Yaroslav Fedyna Date: Sun, 24 May 2026 14:54:46 +0300 Subject: [PATCH 27/27] fix: convert file-scoped namespaces to block namespaces (S3903) SonarCloud rule S3903 flags types not in a named namespace. File-scoped namespace syntax (namespace X;) is not recognized by older SonarCloud C# plugin versions. Converted all EchoTcpServer and EchoTcpServerTests files to block-style namespaces to resolve the two open issues. --- EchoTcpServer/EchoServer.cs | 101 +++--- EchoTcpServer/Networking/ITcpListener.cs | 13 +- .../Networking/TcpListenerWrapper.cs | 29 +- EchoTcpServer/UdpTimedSender.cs | 107 +++---- EchoTcpServerTests/EchoServerTests.cs | 287 +++++++++--------- EchoTcpServerTests/UdpTimedSenderTests.cs | 91 +++--- 6 files changed, 317 insertions(+), 311 deletions(-) diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index 2a36f0d..672417a 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -1,71 +1,72 @@ using EchoTcpServer.Networking; -namespace EchoTcpServer; - -public class EchoServer +namespace EchoTcpServer { - private readonly ITcpListener _listener; - private readonly Action _log; - private readonly CancellationTokenSource _cts = new(); - - public EchoServer(ITcpListener listener, Action? log = null) + public class EchoServer { - _listener = listener; - _log = log ?? Console.WriteLine; - } + private readonly ITcpListener _listener; + private readonly Action _log; + private readonly CancellationTokenSource _cts = new(); - public async Task StartAsync() - { - _listener.Start(); - _log("Server started."); + public EchoServer(ITcpListener listener, Action? log = null) + { + _listener = listener; + _log = log ?? Console.WriteLine; + } - while (!_cts.Token.IsCancellationRequested) + public async Task StartAsync() { - try - { - var stream = await _listener.AcceptClientStreamAsync(); - _log("Client connected."); - _ = Task.Run(() => HandleClientAsync(stream, _cts.Token)); - } - catch (ObjectDisposedException) + _listener.Start(); + _log("Server started."); + + while (!_cts.Token.IsCancellationRequested) { - break; + try + { + var stream = await _listener.AcceptClientStreamAsync(); + _log("Client connected."); + _ = Task.Run(() => HandleClientAsync(stream, _cts.Token)); + } + catch (ObjectDisposedException) + { + break; + } } - } - _log("Server shutdown."); - } + _log("Server shutdown."); + } - public async Task HandleClientAsync(Stream stream, CancellationToken token) - { - using (stream) + public async Task HandleClientAsync(Stream stream, CancellationToken token) { - try + using (stream) { - byte[] buffer = new byte[8192]; - int bytesRead; + try + { + byte[] buffer = new byte[8192]; + int bytesRead; - while (!token.IsCancellationRequested - && (bytesRead = await stream.ReadAsync(buffer.AsMemory(), token)) > 0) + while (!token.IsCancellationRequested + && (bytesRead = await stream.ReadAsync(buffer.AsMemory(), token)) > 0) + { + await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); + _log($"Echoed {bytesRead} bytes."); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) { - await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); - _log($"Echoed {bytesRead} bytes."); + _log($"Error: {ex.Message}"); } } - catch (Exception ex) when (ex is not OperationCanceledException) - { - _log($"Error: {ex.Message}"); - } - } - _log("Client disconnected."); - } + _log("Client disconnected."); + } - public void Stop() - { - _cts.Cancel(); - _listener.Stop(); - _cts.Dispose(); - _log("Server stopped."); + public void Stop() + { + _cts.Cancel(); + _listener.Stop(); + _cts.Dispose(); + _log("Server stopped."); + } } } diff --git a/EchoTcpServer/Networking/ITcpListener.cs b/EchoTcpServer/Networking/ITcpListener.cs index 9952cc0..32ed125 100644 --- a/EchoTcpServer/Networking/ITcpListener.cs +++ b/EchoTcpServer/Networking/ITcpListener.cs @@ -1,8 +1,9 @@ -namespace EchoTcpServer.Networking; - -public interface ITcpListener +namespace EchoTcpServer.Networking { - void Start(); - void Stop(); - Task AcceptClientStreamAsync(); + public interface ITcpListener + { + void Start(); + void Stop(); + Task AcceptClientStreamAsync(); + } } diff --git a/EchoTcpServer/Networking/TcpListenerWrapper.cs b/EchoTcpServer/Networking/TcpListenerWrapper.cs index 67b84a7..44a0ab4 100644 --- a/EchoTcpServer/Networking/TcpListenerWrapper.cs +++ b/EchoTcpServer/Networking/TcpListenerWrapper.cs @@ -1,23 +1,24 @@ using System.Net; using System.Net.Sockets; -namespace EchoTcpServer.Networking; - -public class TcpListenerWrapper : ITcpListener +namespace EchoTcpServer.Networking { - private readonly TcpListener _listener; - - public TcpListenerWrapper(int port) + public class TcpListenerWrapper : ITcpListener { - _listener = new TcpListener(IPAddress.Any, port); - } + private readonly TcpListener _listener; - public void Start() => _listener.Start(); - public void Stop() => _listener.Stop(); + public TcpListenerWrapper(int port) + { + _listener = new TcpListener(IPAddress.Any, port); + } - public async Task AcceptClientStreamAsync() - { - var client = await _listener.AcceptTcpClientAsync(); - return client.GetStream(); + public void Start() => _listener.Start(); + public void Stop() => _listener.Stop(); + + public async Task AcceptClientStreamAsync() + { + var client = await _listener.AcceptTcpClientAsync(); + return client.GetStream(); + } } } diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs index ef9d7a2..7b154a8 100644 --- a/EchoTcpServer/UdpTimedSender.cs +++ b/EchoTcpServer/UdpTimedSender.cs @@ -1,73 +1,74 @@ using System.Net; using System.Net.Sockets; -namespace EchoTcpServer; - -public class UdpTimedSender : IDisposable +namespace EchoTcpServer { - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private readonly Random _rnd = new(); - private Timer? _timer; - private ushort _sequenceNumber; - - public UdpTimedSender(string host, int port) + public class UdpTimedSender : IDisposable { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private readonly Random _rnd = new(); + private Timer? _timer; + private ushort _sequenceNumber; - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); + public UdpTimedSender(string host, int port) + { + _host = host; + _port = port; + _udpClient = new UdpClient(); + } - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } + public void StartSending(int intervalMilliseconds) + { + if (_timer != null) + throw new InvalidOperationException("Sender is already running."); - private void SendMessageCallback(object? state) - { - try + _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); + } + + private void SendMessageCallback(object? state) { - byte[] samples = new byte[1024]; - _rnd.NextBytes(samples); - _sequenceNumber++; + try + { + byte[] samples = new byte[1024]; + _rnd.NextBytes(samples); + _sequenceNumber++; - byte[] msg = new byte[] { 0x04, 0x84 } - .Concat(BitConverter.GetBytes(_sequenceNumber)) - .Concat(samples) - .ToArray(); + byte[] msg = new byte[] { 0x04, 0x84 } + .Concat(BitConverter.GetBytes(_sequenceNumber)) + .Concat(samples) + .ToArray(); - var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port}"); + 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}"); + } } - catch (Exception ex) + + public void StopSending() { - Console.WriteLine($"Error sending message: {ex.Message}"); + _timer?.Dispose(); + _timer = null; } - } - - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposing) - { - if (disposing) + protected virtual void Dispose(bool disposing) { - StopSending(); - _udpClient.Dispose(); + if (disposing) + { + StopSending(); + _udpClient.Dispose(); + } } } } diff --git a/EchoTcpServerTests/EchoServerTests.cs b/EchoTcpServerTests/EchoServerTests.cs index 19e9ac9..02b4363 100644 --- a/EchoTcpServerTests/EchoServerTests.cs +++ b/EchoTcpServerTests/EchoServerTests.cs @@ -2,188 +2,189 @@ using EchoTcpServer; using EchoTcpServer.Networking; -namespace EchoTcpServerTests; - -[TestFixture] -public class EchoServerTests +namespace EchoTcpServerTests { - private Mock _listenerMock = null!; - private List _logs = null!; - private EchoServer _server = null!; - - [SetUp] - public void SetUp() - { - _listenerMock = new Mock(); - _logs = new List(); - _server = new EchoServer(_listenerMock.Object, msg => _logs.Add(msg)); - } - - [Test] - public void Stop_StopsListenerAndLogs() + [TestFixture] + public class EchoServerTests { - _server.Stop(); + private Mock _listenerMock = null!; + private List _logs = null!; + private EchoServer _server = null!; - _listenerMock.Verify(l => l.Stop(), Times.Once); - Assert.That(_logs, Contains.Item("Server stopped.")); - } + [SetUp] + public void SetUp() + { + _listenerMock = new Mock(); + _logs = new List(); + _server = new EchoServer(_listenerMock.Object, msg => _logs.Add(msg)); + } - [Test] - public async Task StartAsync_StartsListenerAndLogsShutdown() - { - _listenerMock - .Setup(l => l.AcceptClientStreamAsync()) - .ThrowsAsync(new ObjectDisposedException("listener")); + [Test] + public void Stop_StopsListenerAndLogs() + { + _server.Stop(); - await _server.StartAsync(); + _listenerMock.Verify(l => l.Stop(), Times.Once); + Assert.That(_logs, Contains.Item("Server stopped.")); + } - _listenerMock.Verify(l => l.Start(), Times.Once); - Assert.Multiple(() => + [Test] + public async Task StartAsync_StartsListenerAndLogsShutdown() { - Assert.That(_logs, Contains.Item("Server started.")); - Assert.That(_logs, Contains.Item("Server shutdown.")); - }); - } + _listenerMock + .Setup(l => l.AcceptClientStreamAsync()) + .ThrowsAsync(new ObjectDisposedException("listener")); + + await _server.StartAsync(); + + _listenerMock.Verify(l => l.Start(), Times.Once); + Assert.Multiple(() => + { + Assert.That(_logs, Contains.Item("Server started.")); + Assert.That(_logs, Contains.Item("Server shutdown.")); + }); + } + + [Test] + public async Task StartAsync_AcceptsClientAndLogs() + { + _listenerMock.SetupSequence(l => l.AcceptClientStreamAsync()) + .ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })) + .ThrowsAsync(new ObjectDisposedException("listener")); - [Test] - public async Task StartAsync_AcceptsClientAndLogs() - { - _listenerMock.SetupSequence(l => l.AcceptClientStreamAsync()) - .ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })) - .ThrowsAsync(new ObjectDisposedException("listener")); + await _server.StartAsync(); - await _server.StartAsync(); + _listenerMock.Verify(l => l.AcceptClientStreamAsync(), Times.Exactly(2)); + Assert.That(_logs, Contains.Item("Client connected.")); + } - _listenerMock.Verify(l => l.AcceptClientStreamAsync(), Times.Exactly(2)); - Assert.That(_logs, Contains.Item("Client connected.")); - } + [Test] + public async Task HandleClientAsync_EchoesInputToOutput() + { + var inputData = new byte[] { 0x41, 0x42, 0x43 }; // "ABC" + var inputStream = new MemoryStream(inputData); + var outputStream = new MemoryStream(); + var stream = new DuplexStream(inputStream, outputStream); - [Test] - public async Task HandleClientAsync_EchoesInputToOutput() - { - var inputData = new byte[] { 0x41, 0x42, 0x43 }; // "ABC" - var inputStream = new MemoryStream(inputData); - var outputStream = new MemoryStream(); - var stream = new DuplexStream(inputStream, outputStream); + await _server.HandleClientAsync(stream, CancellationToken.None); - await _server.HandleClientAsync(stream, CancellationToken.None); + Assert.That(outputStream.ToArray(), Is.EqualTo(inputData)); + } - Assert.That(outputStream.ToArray(), Is.EqualTo(inputData)); - } + [Test] + public async Task HandleClientAsync_LogsEchoedBytes() + { + var stream = new DuplexStream(new MemoryStream(new byte[] { 1, 2 }), new MemoryStream()); - [Test] - public async Task HandleClientAsync_LogsEchoedBytes() - { - var stream = new DuplexStream(new MemoryStream(new byte[] { 1, 2 }), new MemoryStream()); + await _server.HandleClientAsync(stream, CancellationToken.None); - await _server.HandleClientAsync(stream, CancellationToken.None); + Assert.Multiple(() => + { + Assert.That(_logs.Any(m => m.StartsWith("Echoed")), Is.True); + Assert.That(_logs, Contains.Item("Client disconnected.")); + }); + } - Assert.Multiple(() => + [Test] + public async Task HandleClientAsync_PreCancelledToken_DoesNotRead() { - Assert.That(_logs.Any(m => m.StartsWith("Echoed")), Is.True); - Assert.That(_logs, Contains.Item("Client disconnected.")); - }); - } - - [Test] - public async Task HandleClientAsync_PreCancelledToken_DoesNotRead() - { - var cts = new CancellationTokenSource(); - cts.Cancel(); + var cts = new CancellationTokenSource(); + cts.Cancel(); - var stream = new DuplexStream(new MemoryStream(), new MemoryStream()); - await _server.HandleClientAsync(stream, cts.Token); + var stream = new DuplexStream(new MemoryStream(), new MemoryStream()); + await _server.HandleClientAsync(stream, cts.Token); - Assert.That(_logs, Contains.Item("Client disconnected.")); - } + Assert.That(_logs, Contains.Item("Client disconnected.")); + } - [Test] - public async Task HandleClientAsync_StreamException_LogsError() - { - await _server.HandleClientAsync(new ErrorStream(), CancellationToken.None); + [Test] + public async Task HandleClientAsync_StreamException_LogsError() + { + await _server.HandleClientAsync(new ErrorStream(), CancellationToken.None); - Assert.That(_logs.Any(m => m.StartsWith("Error:")), Is.True); - } + Assert.That(_logs.Any(m => m.StartsWith("Error:")), Is.True); + } - [Test] - public async Task HandleClientAsync_EmptyStream_LogsDisconnected() - { - var emptyStream = new DuplexStream(new MemoryStream(Array.Empty()), new MemoryStream()); + [Test] + public async Task HandleClientAsync_EmptyStream_LogsDisconnected() + { + var emptyStream = new DuplexStream(new MemoryStream(Array.Empty()), new MemoryStream()); - await _server.HandleClientAsync(emptyStream, CancellationToken.None); + await _server.HandleClientAsync(emptyStream, CancellationToken.None); - Assert.That(_logs, Contains.Item("Client disconnected.")); + Assert.That(_logs, Contains.Item("Client disconnected.")); + } } -} - -/// -/// Reads from one stream, writes to another — simulates a bidirectional client connection. -/// -internal sealed class DuplexStream : Stream -{ - private readonly Stream _read; - private readonly Stream _write; - public DuplexStream(Stream read, Stream write) + /// + /// Reads from one stream, writes to another — simulates a bidirectional client connection. + /// + internal sealed class DuplexStream : Stream { - _read = read; - _write = write; - } + private readonly Stream _read; + private readonly Stream _write; - public override bool CanRead => true; - public override bool CanSeek => false; - public override bool CanWrite => true; - public override long Length => throw new NotSupportedException(); - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } + public DuplexStream(Stream read, Stream write) + { + _read = read; + _write = write; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } - public override void Flush() => _write.Flush(); - public override int Read(byte[] buffer, int offset, int count) => _read.Read(buffer, offset, count); + public override void Flush() => _write.Flush(); + public override int Read(byte[] buffer, int offset, int count) => _read.Read(buffer, offset, count); - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) - => _read.ReadAsync(buffer, offset, count, ct); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _read.ReadAsync(buffer, offset, count, ct); - public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) - => _read.ReadAsync(buffer, ct); + public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) + => _read.ReadAsync(buffer, ct); - public override void Write(byte[] buffer, int offset, int count) => _write.Write(buffer, offset, count); + public override void Write(byte[] buffer, int offset, int count) => _write.Write(buffer, offset, count); - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) - => _write.WriteAsync(buffer, offset, count, ct); + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _write.WriteAsync(buffer, offset, count, ct); - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ct = default) - => _write.WriteAsync(buffer, ct); + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ct = default) + => _write.WriteAsync(buffer, ct); - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); -} + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + } -/// -/// Stream that throws IOException on every read — simulates a broken connection. -/// -internal sealed class ErrorStream : Stream -{ - public override bool CanRead => true; - public override bool CanWrite => true; - public override bool CanSeek => false; - public override long Length => throw new NotSupportedException(); - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + /// + /// Stream that throws IOException on every read — simulates a broken connection. + /// + internal sealed class ErrorStream : Stream + { + public override bool CanRead => true; + public override bool CanWrite => true; + public override bool CanSeek => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - public override void Flush() { } + public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) => - throw new IOException("Connection reset by peer"); + public override int Read(byte[] buffer, int offset, int count) => + throw new IOException("Connection reset by peer"); - public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) => - ValueTask.FromException(new IOException("Connection reset by peer")); + public override ValueTask ReadAsync(Memory buffer, CancellationToken ct = default) => + ValueTask.FromException(new IOException("Connection reset by peer")); - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) => - Task.FromException(new IOException("Connection reset by peer")); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) => + Task.FromException(new IOException("Connection reset by peer")); - public override void Write(byte[] buffer, int offset, int count) { } - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + } } diff --git a/EchoTcpServerTests/UdpTimedSenderTests.cs b/EchoTcpServerTests/UdpTimedSenderTests.cs index 616b8b7..fb73a25 100644 --- a/EchoTcpServerTests/UdpTimedSenderTests.cs +++ b/EchoTcpServerTests/UdpTimedSenderTests.cs @@ -1,51 +1,52 @@ using EchoTcpServer; -namespace EchoTcpServerTests; - -[TestFixture] -public class UdpTimedSenderTests +namespace EchoTcpServerTests { - [Test] - public void StartSending_WhenAlreadyRunning_ThrowsInvalidOperationException() - { - using var sender = new UdpTimedSender("127.0.0.1", 19999); - sender.StartSending(60_000); - - Assert.Throws(() => sender.StartSending(60_000)); - } - - [Test] - public void StopSending_WithoutStarting_DoesNotThrow() - { - using var sender = new UdpTimedSender("127.0.0.1", 19999); - - Assert.DoesNotThrow(() => sender.StopSending()); - } - - [Test] - public void StopSending_AfterStart_AllowsSecondStart() + [TestFixture] + public class UdpTimedSenderTests { - using var sender = new UdpTimedSender("127.0.0.1", 19999); - sender.StartSending(60_000); - sender.StopSending(); - - Assert.DoesNotThrow(() => sender.StartSending(60_000)); - } - - [Test] - public void Dispose_WithoutStarting_DoesNotThrow() - { - var sender = new UdpTimedSender("127.0.0.1", 19999); - - Assert.DoesNotThrow(() => sender.Dispose()); - } - - [Test] - public void Dispose_WhileSending_DoesNotThrow() - { - var sender = new UdpTimedSender("127.0.0.1", 19999); - sender.StartSending(60_000); - - Assert.DoesNotThrow(() => sender.Dispose()); + [Test] + public void StartSending_WhenAlreadyRunning_ThrowsInvalidOperationException() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.Throws(() => sender.StartSending(60_000)); + } + + [Test] + public void StopSending_WithoutStarting_DoesNotThrow() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.StopSending()); + } + + [Test] + public void StopSending_AfterStart_AllowsSecondStart() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + sender.StopSending(); + + Assert.DoesNotThrow(() => sender.StartSending(60_000)); + } + + [Test] + public void Dispose_WithoutStarting_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.Dispose()); + } + + [Test] + public void Dispose_WhileSending_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.DoesNotThrow(() => sender.Dispose()); + } } }