From 000f1c9f1a57aabdb7bbd43478aba2271e513e62 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 04:44:18 +0200 Subject: [PATCH 01/22] Add SonarCloud configuration --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e7840696..6e077c38 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -56,8 +56,8 @@ jobs: dotnet tool install --global dotnet-sonarscanner echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` - /k:"ppanchen_NetSdrClient" ` - /o:"ppanchen" ` + /d:sonar.projectKey="YuraKozyyr_NetSdrClient" ` + /d:sonar.organization="yurakozyr" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` From 90508b002a0d8dfe5342399d4861861f5a859cf5 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 04:48:23 +0200 Subject: [PATCH 02/22] Fixed SonarCloud configuration --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 6e077c38..acf4e7b5 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -56,8 +56,8 @@ jobs: dotnet tool install --global dotnet-sonarscanner echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` - /d:sonar.projectKey="YuraKozyyr_NetSdrClient" ` - /d:sonar.organization="yurakozyr" ` + /k:"YuraKozyyr_NetSdrClient" ` + /o:"yurakozyr" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` From fa223f9326f6fb3b52b93b1310bab80dc3a60464 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 05:43:42 +0200 Subject: [PATCH 03/22] Test PR decoration --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b3a90294..5c0b06d1 100644 --- a/README.md +++ b/README.md @@ -276,3 +276,5 @@ updates: Обмежити умову запуску Sonar: тільки PR **або** `refs/heads/master`. - **PR зелений, push червоний** Перевірити **New Code Definition** (Number of days або Previous version) і довести покриття/дублікації на “new code”. + + From a6f3245835ab06ec3e08ca6479f7bba9478409ad Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 05:47:38 +0200 Subject: [PATCH 04/22] Test PR decoration2 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5c0b06d1..3c9737e4 100644 --- a/README.md +++ b/README.md @@ -278,3 +278,4 @@ updates: Перевірити **New Code Definition** (Number of days або Previous version) і довести покриття/дублікації на “new code”. + From 6208492fab4511edac92ad6c5ff43491f8f2cef6 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 05:48:36 +0200 Subject: [PATCH 05/22] Test PR decoration --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3c9737e4..a397be78 100644 --- a/README.md +++ b/README.md @@ -279,3 +279,4 @@ updates: + From 5bfb80e44b6ba6edb7ee9137723a1bb283b0fbbe Mon Sep 17 00:00:00 2001 From: YuraKozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 05:56:17 +0200 Subject: [PATCH 06/22] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a397be78..4a2f6c9e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Лабораторні з реінжинірингу (8×) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=bugs)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=coverage)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=bugs)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=YuraKozyyr_NetSdrClient&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient) Цей репозиторій використовується для курсу **реінжиніринг ПЗ**. From 90542002aee736754608b3cf4b8bba8090dcd9a0 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 06:19:24 +0200 Subject: [PATCH 07/22] Refactor: Make client fields readonly to fix Sonar smell --- NetSdrClientApp/NetSdrClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c058..c4908a62 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -14,8 +14,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; } From 400eba54ebc9a9b1b423bfc4674695ffb19f582d Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 06:26:14 +0200 Subject: [PATCH 08/22] Refactor: Removed empty statement to fix Sonar smell --- NetSdrClientApp/NetSdrClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index c4908a62..dbf6cb84 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -66,7 +66,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; From c30c83c508733a65102e8e2fe8ee6b0062c0762b Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 06:35:21 +0200 Subject: [PATCH 09/22] Refactor: Made '_udpClient_MessageReceived' a static method to fix Sonar smell --- NetSdrClientApp/NetSdrClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index dbf6cb84..0c2cf22e 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -114,7 +114,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 MsgTypes type, out ControlItemCodes code, out ushort sequenceNum, out byte[] body); var samples = NetSdrMessageHelper.GetSamples(16, body); From 7c448ce92f22d9186d8a644c00c588e054949e7e Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 06:36:53 +0200 Subject: [PATCH 10/22] Refactor: Removed unused variable to fix Sonar smell --- NetSdrClientApp/NetSdrClient.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index 0c2cf22e..29ac70d1 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -116,7 +116,12 @@ public async Task ChangeFrequencyAsync(long hz, int channel) private static 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 _, // MsgTypes type не потрібен + out _, // ControlItemCodes code не потрібен + out _, // ushort sequenceNum не потрібен + out var body); // тільки body використовуємо var samples = NetSdrMessageHelper.GetSamples(16, body); Console.WriteLine($"Samples recieved: " + body.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); From 2355d2da482db6e692aac944535cffe9a8faa5c7 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 07:43:06 +0200 Subject: [PATCH 11/22] Lab3: tests & coverage --- .github/workflows/sonarcloud.yml | 14 +++++++------- NetSdrClientAppTests/NetSdrClientAppTests.csproj | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index acf4e7b5..0eb80cb1 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -70,13 +70,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 }}" diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46af..0a44ab1d 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -11,7 +11,11 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + From fef148cbee53e1b4e1d376d674d00e524aad0f6b Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Tue, 2 Dec 2025 22:20:10 +0200 Subject: [PATCH 12/22] Lab4:Clear Duplications --- .../Networking/TcpClientWrapper.cs | 25 +++++++++---------- .../Networking/UdpClientWrapper.cs | 22 ++++++++-------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e5..e767c7de 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -70,12 +70,14 @@ public void Disconnect() Console.WriteLine("No active connection to disconnect."); } } - - public async Task SendMessageAsync(byte[] data) + private async Task SendAsync(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}")); + Console.WriteLine("Message sent: " + + data.Select(b => Convert.ToString(b, 16)) + .Aggregate((l, r) => $"{l} {r}")); + await _stream.WriteAsync(data, 0, data.Length); } else @@ -84,18 +86,15 @@ public async Task SendMessageAsync(byte[] data) } } - public async Task SendMessageAsync(string str) + public Task SendMessageAsync(byte[] data) + { + return SendAsync(data); + } + + public Task SendMessageAsync(string str) { 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}")); - await _stream.WriteAsync(data, 0, data.Length); - } - else - { - throw new InvalidOperationException("Not connected to a server."); - } + return SendAsync(data); } private async Task StartListeningAsync() diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b798..71104cb7 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -44,8 +44,8 @@ public async Task StartListeningAsync() Console.WriteLine($"Error receiving message: {ex.Message}"); } } - - public void StopListening() + + private void StopInternal() { try { @@ -59,20 +59,18 @@ public void StopListening() } } + + public void StopListening() + { + StopInternal(); + } + 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}"); - } + StopInternal(); } + public override int GetHashCode() { var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; From dbef6080e3a08478e3ffbc9a85a76a9834d8606a Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 00:19:29 +0200 Subject: [PATCH 13/22] Refactor wrappers to reduce duplication --- .../Networking/TcpClientWrapper.cs | 28 ++++++++-------- .../Networking/UdpClientWrapper.cs | 33 ++++++++++++------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index e767c7de..a07ae78d 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -70,13 +70,24 @@ public void Disconnect() Console.WriteLine("No active connection to disconnect."); } } - private async Task SendAsync(byte[] data) + public Task SendMessageAsync(byte[] data) + { + return SendMessageInternalAsync(data); + } + + public Task SendMessageAsync(string str) + { + var data = Encoding.UTF8.GetBytes(str); + return SendMessageInternalAsync(data); + } + + private async Task SendMessageInternalAsync(byte[] data) { if (Connected && _stream != null && _stream.CanWrite) { Console.WriteLine("Message sent: " + - data.Select(b => Convert.ToString(b, 16)) - .Aggregate((l, r) => $"{l} {r}")); + data.Select(b => Convert.ToString(b, toBase: 16)) + .Aggregate((l, r) => $"{l} {r}")); await _stream.WriteAsync(data, 0, data.Length); } @@ -86,17 +97,6 @@ private async Task SendAsync(byte[] data) } } - public Task SendMessageAsync(byte[] data) - { - return SendAsync(data); - } - - public Task SendMessageAsync(string str) - { - var data = Encoding.UTF8.GetBytes(str); - return SendAsync(data); - } - private async Task StartListeningAsync() { if (Connected && _stream != null && _stream.CanRead) diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 71104cb7..ac55757d 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -45,12 +45,32 @@ public async Task StartListeningAsync() } } - private void StopInternal() + public void StopListening() + { + StopInternal("StopListening"); + } + + public void Exit() + { + StopInternal("Exit"); + } + + private void StopInternal(string reason) { try { + if (_cts == null && _udpClient == null) + { + Console.WriteLine($"UDP listener already stopped ({reason})."); + return; + } + _cts?.Cancel(); _udpClient?.Close(); + + _cts = null; + _udpClient = null; + Console.WriteLine("Stopped listening for UDP messages."); } catch (Exception ex) @@ -60,17 +80,6 @@ private void StopInternal() } - public void StopListening() - { - StopInternal(); - } - - public void Exit() - { - StopInternal(); - } - - public override int GetHashCode() { var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; From 7ebe25df5442a029dfe6a78e1933d5a3786cc036 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 00:56:18 +0200 Subject: [PATCH 14/22] Add NUnit tests for Tcp/Udp wrappers --- NetSdrClientAppTests/TcpClientWrapperTests.cs | 58 +++++++++++++++ NetSdrClientAppTests/UdpClientWrapperTests.cs | 72 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 NetSdrClientAppTests/TcpClientWrapperTests.cs create mode 100644 NetSdrClientAppTests/UdpClientWrapperTests.cs diff --git a/NetSdrClientAppTests/TcpClientWrapperTests.cs b/NetSdrClientAppTests/TcpClientWrapperTests.cs new file mode 100644 index 00000000..1c3f8b85 --- /dev/null +++ b/NetSdrClientAppTests/TcpClientWrapperTests.cs @@ -0,0 +1,58 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using NetSdrClientApp.Networking; +using NUnit.Framework; + +namespace NetSdrClientAppTests.Networking +{ + [TestFixture] + public class TcpClientWrapperTests + { + [Test] + public void Disconnect_WhenNotConnected_DoesNotThrow() + { + var wrapper = new TcpClientWrapper("localhost", 5555); + + Assert.DoesNotThrow(() => wrapper.Disconnect()); + Assert.False(wrapper.Connected); + } + + [Test] + public async Task Connect_And_SendMessage_WorksWithLocalListener() + { + // arrange: піднімаємо локальний TcpListener на вільному порту + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var wrapper = new TcpClientWrapper("127.0.0.1", port); + + // приймаємо клієнта в бекґраунді + var acceptTask = listener.AcceptTcpClientAsync(); + + // act + Assert.DoesNotThrow(() => wrapper.Connect()); + Assert.True(wrapper.Connected, "Wrapper should be connected after successful Connect()."); + + var message = "hello"; + await wrapper.SendMessageAsync(message); + + // assert: сервер реально щось отримав + using var serverClient = await acceptTask; + using var stream = serverClient.GetStream(); + + var buffer = new byte[message.Length]; + var read = await stream.ReadAsync(buffer, 0, buffer.Length); + var received = Encoding.UTF8.GetString(buffer, 0, read); + + Assert.AreEqual(message, received); + + // cleanup + wrapper.Disconnect(); + listener.Stop(); + } + } +} + diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs new file mode 100644 index 00000000..cbca5ca5 --- /dev/null +++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using NetSdrClientApp.Networking; +using NUnit.Framework; + +namespace NetSdrClientAppTests.Networking +{ + [TestFixture] + public class UdpClientWrapperTests + { + [Test] + public void Exit_WhenNotStarted_DoesNotThrow() + { + var wrapper = new UdpClientWrapper(5555); + + Assert.DoesNotThrow(() => wrapper.Exit()); + } + + [Test] + public void GetHashCode_IsStableForSamePort_AndDifferentForOtherPort() + { + var w1 = new UdpClientWrapper(5555); + var w2 = new UdpClientWrapper(5555); + var w3 = new UdpClientWrapper(5556); + + var hash1 = w1.GetHashCode(); + var hash2 = w2.GetHashCode(); + var hash3 = w3.GetHashCode(); + + Assert.AreEqual(hash1, hash2, "Hash must be stable for однакових параметрів."); + Assert.AreNotEqual(hash1, hash3, "Hash має відрізнятися для різних портів."); + } + + [Test] + public async Task StartListeningAsync_RaisesMessageReceived_WhenPacketArrives() + { + // спочатку займаємо вільний UDP порт + int port; + using (var probe = new UdpClient(0)) + { + port = ((IPEndPoint)probe.Client.LocalEndPoint!).Port; + } + + var wrapper = new UdpClientWrapper(port); + + var tcs = new TaskCompletionSource(); + wrapper.MessageReceived += (_, data) => tcs.TrySetResult(data); + + var listeningTask = wrapper.StartListeningAsync(); + + // відправляємо пакет на цей порт + using (var sender = new UdpClient()) + { + var payload = new byte[] { 1, 2, 3 }; + await sender.SendAsync(payload, payload.Length, "127.0.0.1", port); + + // чекаємо максимум 1 секунду + var completed = await Task.WhenAny(tcs.Task, Task.Delay(1000)); + Assert.AreSame(tcs.Task, completed, "Повідомлення не було отримано за таймаут."); + + CollectionAssert.AreEqual(payload, tcs.Task.Result); + } + + wrapper.StopListening(); + + // даємо задачі акуратно завершитися, але не блокуємось вічно + await Task.WhenAny(listeningTask, Task.Delay(1000)); + } + } +} + From 21bfd779fc17fbc77176c7a17db7368eec3cee10 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 01:15:39 +0200 Subject: [PATCH 15/22] Improve tests coverage for wrappers --- NetSdrClientAppTests/TcpClientWrapperTests.cs | 26 +++++++++++++++++++ NetSdrClientAppTests/UdpClientWrapperTests.cs | 8 ++++++ 2 files changed, 34 insertions(+) diff --git a/NetSdrClientAppTests/TcpClientWrapperTests.cs b/NetSdrClientAppTests/TcpClientWrapperTests.cs index 1c3f8b85..a6c839af 100644 --- a/NetSdrClientAppTests/TcpClientWrapperTests.cs +++ b/NetSdrClientAppTests/TcpClientWrapperTests.cs @@ -10,6 +10,32 @@ namespace NetSdrClientAppTests.Networking [TestFixture] public class TcpClientWrapperTests { + + [Test] + public void Connect_WhenAlreadyConnected_DoesNotThrow_AndStaysConnected() + { + // arrange + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var wrapper = new TcpClientWrapper("127.0.0.1", port); + + // перше підключення – як у попередньому тесті + wrapper.Connect(); + Assert.IsTrue(wrapper.Connected, "Перше підключення не пройшло."); + + // act + assert: другий виклик Connect() повинен піти в гілку + // `if (Connected) { Console.WriteLine("Already connected..."); return; }` + Assert.DoesNotThrow(() => wrapper.Connect(), "Повторний Connect не повинен кидати виняток."); + Assert.IsTrue(wrapper.Connected, "Після повторного Connect з’єднання має залишатися активним."); + + // cleanup + wrapper.Disconnect(); + listener.Stop(); + } + + [Test] public void Disconnect_WhenNotConnected_DoesNotThrow() { diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs index cbca5ca5..c8c24980 100644 --- a/NetSdrClientAppTests/UdpClientWrapperTests.cs +++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs @@ -9,6 +9,14 @@ namespace NetSdrClientAppTests.Networking [TestFixture] public class UdpClientWrapperTests { + [Test] + public void StopListening_WhenNotStarted_DoesNotThrow() + { + var wrapper = new UdpClientWrapper(5555); + + Assert.DoesNotThrow(() => wrapper.StopListening()); + } + [Test] public void Exit_WhenNotStarted_DoesNotThrow() { From 86a8e75f11141cbb5fad23621498f6f151b8ab7e Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 01:48:37 +0200 Subject: [PATCH 16/22] Improve tests coverage for wrappers --- NetSdrClientAppTests/NetSdrClientTests.cs | 102 ++++++++++++++++------ 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f8..5b9bfb85 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -1,6 +1,11 @@ -using Moq; +using System; +using System.IO; +using System.Threading.Tasks; +using Moq; using NetSdrClientApp; +using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; +using NUnit.Framework; namespace NetSdrClientAppTests; @@ -26,10 +31,11 @@ public void Setup() _tcpMock.Setup(tcp => tcp.Connected).Returns(false); }); - _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())).Callback((bytes) => - { - _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes); - }); + _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())) + .Callback((bytes) => + { + _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes); + }); _updMock = new Mock(); @@ -39,10 +45,8 @@ public void Setup() [Test] public async Task ConnectAsyncTest() { - //act await _client.ConnectAsync(); - //assert _tcpMock.Verify(tcp => tcp.Connect(), Times.Once); _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(3)); } @@ -50,37 +54,26 @@ public async Task ConnectAsyncTest() [Test] public async Task DisconnectWithNoConnectionTest() { - //act _client.Disconect(); - //assert - //No exception thrown _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } [Test] public async Task DisconnectTest() { - //Arrange await ConnectAsyncTest(); - //act _client.Disconect(); - //assert - //No exception thrown _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } [Test] public async Task StartIQNoConnectionTest() { - - //act await _client.StartIQAsync(); - //assert - //No exception thrown _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); _tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce); } @@ -88,14 +81,10 @@ public async Task StartIQNoConnectionTest() [Test] public async Task StartIQTest() { - //Arrange await ConnectAsyncTest(); - //act await _client.StartIQAsync(); - //assert - //No exception thrown _updMock.Verify(udp => udp.StartListeningAsync(), Times.Once); Assert.That(_client.IQStarted, Is.True); } @@ -103,17 +92,76 @@ public async Task StartIQTest() [Test] public async Task StopIQTest() { - //Arrange await ConnectAsyncTest(); - //act await _client.StopIQAsync(); - //assert - //No exception thrown _updMock.Verify(tcp => tcp.StopListening(), Times.Once); Assert.That(_client.IQStarted, Is.False); } - //TODO: cover the rest of the NetSdrClient code here + [Test] + public async Task StopIQAsync_WhenNotConnected_DoesNotThrow() + { + var tcpMock = new Mock(); + tcpMock.SetupGet(t => t.Connected).Returns(false); + + var udpMock = new Mock(); + + var client = new NetSdrClient(tcpMock.Object, udpMock.Object); + + Assert.DoesNotThrowAsync(async () => await client.StopIQAsync()); + } + + [Test] + public async Task ChangeFrequencyAsync_WhenNotConnected_DoesNotThrowAndUsesSendTcpRequestBranch() + { + var tcpMock = new Mock(); + tcpMock.SetupGet(t => t.Connected).Returns(false); + + var udpMock = new Mock(); + + var client = new NetSdrClient(tcpMock.Object, udpMock.Object); + + Assert.DoesNotThrowAsync(async () => await client.ChangeFrequencyAsync(20_000_000, 1)); + } + + [Test] + public void UdpClient_MessageReceived_WritesSamplesToFile() + { + var tcpMock = new Mock(); + tcpMock.SetupGet(t => t.Connected).Returns(true); + + var udpMock = new Mock(); + + EventHandler? handler = null; + + udpMock.SetupAdd(u => u.MessageReceived += It.IsAny>()) + .Callback>(h => handler += h); + + udpMock.SetupRemove(u => u.MessageReceived -= It.IsAny>()) + .Callback>(h => handler -= h); + + var client = new NetSdrClient(tcpMock.Object, udpMock.Object); + + const string fileName = "samples.bin"; + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + var body = new byte[] { 0x00, 0x01, 0x00, 0x02 }; // два 16-бітні семпли + var msg = NetSdrMessageHelper.GetDataItemMessage(NetSdrMessageHelper.MsgTypes.DataItem0, body); + + // емулюємо прихід UDP-повідомлення + handler?.Invoke(this, msg); + + Assert.That(File.Exists(fileName), Is.True, "Файл samples.bin мав бути створений"); + + var length = new FileInfo(fileName).Length; + Assert.That(length, Is.GreaterThan(0), "Файл samples.bin має містити дані"); + + File.Delete(fileName); + } } + From 30b751ad7b22bb0d4d491f6e3f1ff78bd3433edb Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 03:03:39 +0200 Subject: [PATCH 17/22] Lab5: add arch tests (broken state) --- NetSdrClient.sln | 6 ++ .../ArchitectureTests.cs | 87 +++++++++++++++++++ NetSdrClientApp.ArchTests/GlobalUsings.cs | 1 + .../NetSdrClientApp.ArchTests.csproj | 22 +++++ NetSdrClientApp/Networking/BadClient.cs | 7 ++ 5 files changed, 123 insertions(+) create mode 100644 NetSdrClientApp.ArchTests/ArchitectureTests.cs create mode 100644 NetSdrClientApp.ArchTests/GlobalUsings.cs create mode 100644 NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj create mode 100644 NetSdrClientApp/Networking/BadClient.cs diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb3..3e482753 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,6 +9,8 @@ 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}") = "NetSdrClientApp.ArchTests", "NetSdrClientApp.ArchTests\NetSdrClientApp.ArchTests.csproj", "{28EB108C-651C-4289-9900-3AAE9BB597F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU + {28EB108C-651C-4289-9900-3AAE9BB597F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28EB108C-651C-4289-9900-3AAE9BB597F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28EB108C-651C-4289-9900-3AAE9BB597F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28EB108C-651C-4289-9900-3AAE9BB597F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NetSdrClientApp.ArchTests/ArchitectureTests.cs b/NetSdrClientApp.ArchTests/ArchitectureTests.cs new file mode 100644 index 00000000..99808861 --- /dev/null +++ b/NetSdrClientApp.ArchTests/ArchitectureTests.cs @@ -0,0 +1,87 @@ +using NetArchTest.Rules; +using NetSdrClientApp; +using NetSdrClientApp.Messages; +using NetSdrClientApp.Networking; +using NUnit.Framework; + +namespace NetSdrClientApp.ArchTests; + +[TestFixture] +public class ArchitectureTests +{ + /// + /// Головна збірка клієнта не повинна залежати від тестового Echo-сервера. + /// (Echo-сервер – лише допоміжний інструмент для тестування) + /// + [Test] + public void NetSdrClientApp_Should_Not_Depend_On_EchoTcpServer() + { + var result = Types + .InAssembly(typeof(NetSdrClient).Assembly) + .ShouldNot() + .HaveDependencyOn("EchoServer") // назва збірки EchoTcpServer + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "NetSdrClientApp не повинен мати залежність від EchoTcpServer (EchoServer)."); + } + + /// + /// Повідомлення (Messages) не мають тягнути за собою залежності від мережевого шару. + /// + [Test] + public void Messages_Should_Not_Depend_On_Networking() + { + var result = Types + .InAssembly(typeof(NetSdrMessageHelper).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Messages") + .ShouldNot() + .HaveDependencyOn("NetSdrClientApp.Networking") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "NetSdrClientApp.Messages не повинен залежати від NetSdrClientApp.Networking."); + } + + /// + /// Навпаки: мережевий шар не має залежати від Messages, + /// щоб уникнути циклічних залежностей. + /// + [Test] + public void Networking_Should_Not_Depend_On_Messages() + { + var result = Types + .InAssembly(typeof(TcpClientWrapper).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Networking") + .ShouldNot() + .HaveDependencyOn("NetSdrClientApp.Messages") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "NetSdrClientApp.Networking не повинен залежати від NetSdrClientApp.Messages."); + } + + /// + /// Усі типи в просторі імен Networking мають бути *Wrapper*-ами. + /// + [Test] + public void Networking_Types_Should_Have_Names_Ending_With_Wrapper() + { + var result = Types + .InAssembly(typeof(TcpClientWrapper).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Networking") + .And() + .AreClasses() // 🔹 важливо: тільки класи, без інтерфейсів + .Should() + .HaveNameEndingWith("Wrapper") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "У NetSdrClientApp.Networking мають бути лише класи, назва яких закінчується на 'Wrapper'."); + } + +} + diff --git a/NetSdrClientApp.ArchTests/GlobalUsings.cs b/NetSdrClientApp.ArchTests/GlobalUsings.cs new file mode 100644 index 00000000..cefced49 --- /dev/null +++ b/NetSdrClientApp.ArchTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj b/NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj new file mode 100644 index 00000000..fa9e0620 --- /dev/null +++ b/NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + diff --git a/NetSdrClientApp/Networking/BadClient.cs b/NetSdrClientApp/Networking/BadClient.cs new file mode 100644 index 00000000..cb2e87b0 --- /dev/null +++ b/NetSdrClientApp/Networking/BadClient.cs @@ -0,0 +1,7 @@ +// NetSdrClientApp/Networking/BadClient.cs +namespace NetSdrClientApp.Networking; + +public class BadClient +{ +} + From 0fd1ca8a3173457c37aafc29c3e1b79d393207a8 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 04:35:29 +0200 Subject: [PATCH 18/22] Added bad boy to fail test --- .github/workflows/sonarcloud.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 0eb80cb1..e538bdf2 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -76,6 +76,7 @@ jobs: /p:CollectCoverage=true ` /p:CoverletOutput=TestResults/coverage.xml ` /p:CoverletOutputFormat=opencover + dotnet test NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj -c Release --no-build shell: pwsh # 3) END: SonarScanner - name: SonarScanner End From 3ce05099b87dd1660c43fadc210c316344e7e8c2 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 04:41:53 +0200 Subject: [PATCH 19/22] Bab boy removed --- NetSdrClientApp/Networking/BadClient.cs | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 NetSdrClientApp/Networking/BadClient.cs diff --git a/NetSdrClientApp/Networking/BadClient.cs b/NetSdrClientApp/Networking/BadClient.cs deleted file mode 100644 index cb2e87b0..00000000 --- a/NetSdrClientApp/Networking/BadClient.cs +++ /dev/null @@ -1,7 +0,0 @@ -// NetSdrClientApp/Networking/BadClient.cs -namespace NetSdrClientApp.Networking; - -public class BadClient -{ -} - From d48af86a62dd8739dff2436659ed79c0242ac1ea Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 05:23:35 +0200 Subject: [PATCH 20/22] Lab6: Fixed few bugs --- .../Networking/TcpClientWrapper.cs | 166 ++++++++++++------ .../Networking/UdpClientWrapper.cs | 165 +++++++++++------ .../TcpClientWrapperEqualityTests.cs | 38 ++++ .../UdpClientWrapperAdditionalTests.cs | 40 +++++ .../UdpClientWrapperEqualityTests.cs | 55 ++++++ 5 files changed, 354 insertions(+), 110 deletions(-) create mode 100644 NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs create mode 100644 NetSdrClientAppTests/UdpClientWrapperAdditionalTests.cs create mode 100644 NetSdrClientAppTests/UdpClientWrapperEqualityTests.cs diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index a07ae78d..8ef0df0e 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Net.Http; using System.Net.Sockets; using System.Text; using System.Threading; @@ -10,13 +7,15 @@ namespace NetSdrClientApp.Networking { - public class TcpClientWrapper : ITcpClient + public class TcpClientWrapper : ITcpClient, IDisposable { - private string _host; - private int _port; + private readonly string _host; + private readonly int _port; + private TcpClient? _tcpClient; private NetworkStream? _stream; - private CancellationTokenSource _cts; + private CancellationTokenSource? _cts; + private bool _disposed; public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null; @@ -30,46 +29,77 @@ public TcpClientWrapper(string host, int port) public void Connect() { + ThrowIfDisposed(); + if (Connected) { Console.WriteLine($"Already connected to {_host}:{_port}"); return; } + // прибираємо попередню CTS (якщо була) + _cts?.Cancel(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + _tcpClient = new TcpClient(); try { - _cts = new CancellationTokenSource(); _tcpClient.Connect(_host, _port); _stream = _tcpClient.GetStream(); Console.WriteLine($"Connected to {_host}:{_port}"); - _ = StartListeningAsync(); + + // запускаємо слухача з токеном цієї CTS + _ = StartListeningAsync(_cts.Token); } catch (Exception ex) { Console.WriteLine($"Failed to connect: {ex.Message}"); - } - } + // при фейлі не тримаємо зайві ресурси + _stream?.Dispose(); + _tcpClient?.Dispose(); + _cts?.Dispose(); - public void Disconnect() - { - if (Connected) - { - _cts?.Cancel(); - _stream?.Close(); - _tcpClient?.Close(); - - _cts = null; - _tcpClient = null; _stream = null; - Console.WriteLine("Disconnected."); - } - else - { - Console.WriteLine("No active connection to disconnect."); + _tcpClient = null; + _cts = null; } } + + public void Disconnect() + { + // не даємо викликати метод після Dispose() + ThrowIfDisposed(); + + if (!Connected) + { + Console.WriteLine("No active connection to disconnect."); + return; + } + + try + { + _cts?.Cancel(); + _stream?.Close(); + _tcpClient?.Close(); + } + catch (Exception ex) + { + Console.WriteLine($"Error while disconnecting: {ex.Message}"); + } + finally + { + _stream?.Dispose(); + _tcpClient?.Dispose(); + _cts?.Dispose(); + + _stream = null; + _tcpClient = null; + _cts = null; + } + } + public Task SendMessageAsync(byte[] data) { return SendMessageInternalAsync(data); @@ -83,6 +113,8 @@ public Task SendMessageAsync(string str) private async Task SendMessageInternalAsync(byte[] data) { + ThrowIfDisposed(); + if (Connected && _stream != null && _stream.CanWrite) { Console.WriteLine("Message sent: " + @@ -97,43 +129,69 @@ private async Task SendMessageInternalAsync(byte[] data) } } - private async Task StartListeningAsync() + private async Task StartListeningAsync(CancellationToken token) { - if (Connected && _stream != null && _stream.CanRead) + if (_stream == null || !_stream.CanRead) { - try - { - Console.WriteLine($"Starting listening for incomming messages."); + throw new InvalidOperationException("Not connected to a server."); + } - while (!_cts.Token.IsCancellationRequested) - { - byte[] buffer = new byte[8194]; + try + { + Console.WriteLine("Starting listening for incoming messages."); - int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token); - if (bytesRead > 0) - { - MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); - } - } - } - catch (OperationCanceledException ex) - { - //empty - } - catch (Exception ex) - { - Console.WriteLine($"Error in listening loop: {ex.Message}"); - } - finally + var buffer = new byte[8194]; + + while (!token.IsCancellationRequested) { - Console.WriteLine("Listener stopped."); + var bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, token); + if (bytesRead <= 0) + { + // зʼєднання закрите + break; + } + + var data = buffer.AsSpan(0, bytesRead).ToArray(); + MessageReceived?.Invoke(this, data); } } - else + catch (OperationCanceledException) { - throw new InvalidOperationException("Not connected to a server."); + // очікувано при відключенні + } + catch (Exception ex) + { + Console.WriteLine($"Error in listening loop: {ex.Message}"); + } + finally + { + Console.WriteLine("Listener stopped."); } } - } + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(TcpClientWrapper)); + } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + _cts?.Cancel(); + + _stream?.Dispose(); + _tcpClient?.Dispose(); + _cts?.Dispose(); + + _stream = null; + _tcpClient = null; + _cts = null; + } + } } + diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index ac55757d..613ad58d 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,92 +1,145 @@ using System; 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 +namespace NetSdrClientApp.Networking { - private readonly IPEndPoint _localEndPoint; - private CancellationTokenSource? _cts; - private UdpClient? _udpClient; - - public event EventHandler? MessageReceived; - - public UdpClientWrapper(int port) + public sealed class UdpClientWrapper : IUdpClient, IDisposable { - _localEndPoint = new IPEndPoint(IPAddress.Any, port); - } + private readonly IPEndPoint _localEndPoint; + private CancellationTokenSource? _cts; + private UdpClient? _udpClient; + private bool _disposed; - public async Task StartListeningAsync() - { - _cts = new CancellationTokenSource(); - Console.WriteLine("Start listening for UDP messages..."); + public event EventHandler? MessageReceived; + + public UdpClientWrapper(int port) + { + _localEndPoint = new IPEndPoint(IPAddress.Any, port); + } - try + public async Task StartListeningAsync() { - _udpClient = new UdpClient(_localEndPoint); - while (!_cts.Token.IsCancellationRequested) + ThrowIfDisposed(); + + if (_cts is not null) { - UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); - MessageReceived?.Invoke(this, result.Buffer); + Console.WriteLine("UDP listener is already running."); + return; + } + + _cts = new CancellationTokenSource(); + Console.WriteLine("Start listening for UDP messages..."); - Console.WriteLine($"Received from {result.RemoteEndPoint}"); + try + { + _udpClient = new UdpClient(_localEndPoint); + + while (!_cts.IsCancellationRequested) + { + UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); + MessageReceived?.Invoke(this, result.Buffer); + Console.WriteLine($"Received from {result.RemoteEndPoint}"); + } + } + catch (OperationCanceledException) + { + // очікувано при зупинці + } + catch (Exception ex) + { + Console.WriteLine($"Error receiving message: {ex.Message}"); + } + finally + { + // гарантовано звільняємо ресурси + DisposeUdpResources(); } } - catch (OperationCanceledException ex) + + public void StopListening() { - //empty + StopInternal("StopListening"); } - catch (Exception ex) + + public void Exit() { - Console.WriteLine($"Error receiving message: {ex.Message}"); + StopInternal("Exit"); } - } - - public void StopListening() - { - StopInternal("StopListening"); - } - - public void Exit() - { - StopInternal("Exit"); - } - private void StopInternal(string reason) - { - try + private void StopInternal(string reason) { - if (_cts == null && _udpClient == null) + ThrowIfDisposed(); + + try { - Console.WriteLine($"UDP listener already stopped ({reason})."); - return; + if (_cts is null && _udpClient is null) + { + Console.WriteLine($"UDP listener already stopped ({reason})."); + return; + } + + _cts?.Cancel(); + } + catch (Exception ex) + { + Console.WriteLine($"Error while cancelling token: {ex.Message}"); + } + finally + { + DisposeUdpResources(); + Console.WriteLine("Stopped listening for UDP messages."); } + } - _cts?.Cancel(); - _udpClient?.Close(); + private void DisposeUdpResources() + { + _cts?.Dispose(); + _udpClient?.Dispose(); _cts = null; _udpClient = null; + } + + // ---- Рівність та хеш-код без MD5 ---- - Console.WriteLine("Stopped listening for UDP messages."); + public override int GetHashCode() + { + // Без криптографії – просто комбінуємо адресу та порт. + return HashCode.Combine(_localEndPoint.Address, _localEndPoint.Port); } - catch (Exception ex) + + public override bool Equals(object? obj) { - Console.WriteLine($"Error while stopping: {ex.Message}"); + if (ReferenceEquals(this, obj)) + return true; + + if (obj is not UdpClientWrapper other) + return false; + + return Equals(_localEndPoint, other._localEndPoint); } - } + // ---- Dispose pattern ---- - public override int GetHashCode() - { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(UdpClientWrapper)); + } - using var md5 = MD5.Create(); - var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload)); + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; - return BitConverter.ToInt32(hash, 0); + _cts?.Cancel(); + DisposeUdpResources(); + } } -} \ No newline at end of file +} + diff --git a/NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs b/NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs new file mode 100644 index 00000000..a4598390 --- /dev/null +++ b/NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using NetSdrClientApp.Networking; +using NUnit.Framework; + +namespace NetSdrClientAppTests.Networking; + +[TestFixture] +public class TcpClientWrapperDisposalTests +{ + [Test] + public void Disconnect_AfterDispose_ThrowsObjectDisposedException() + { + // arrange + var client = new TcpClientWrapper("127.0.0.1", 12345); + + // act + client.Dispose(); + + // assert + Assert.Throws(() => client.Disconnect()); + } + + [Test] + public void SendMessageAsync_AfterDispose_ThrowsObjectDisposedException() + { + // arrange + var client = new TcpClientWrapper("127.0.0.1", 12345); + + // act + client.Dispose(); + + // assert + Assert.ThrowsAsync(async () => + await client.SendMessageAsync("hello")); + } +} + diff --git a/NetSdrClientAppTests/UdpClientWrapperAdditionalTests.cs b/NetSdrClientAppTests/UdpClientWrapperAdditionalTests.cs new file mode 100644 index 00000000..5f0e0451 --- /dev/null +++ b/NetSdrClientAppTests/UdpClientWrapperAdditionalTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using NetSdrClientApp.Networking; +using NUnit.Framework; + +namespace NetSdrClientAppTests.Networking; + +[TestFixture] +public class UdpClientWrapperAdditionalTests +{ + [Test] + public void StartListeningAsync_AfterDispose_ThrowsObjectDisposedException() + { + // arrange + var client = new UdpClientWrapper(0); // порт не важливий, ми не дійдемо до створення UdpClient + + client.Dispose(); + + // assert + Assert.ThrowsAsync(async () => + await client.StartListeningAsync()); + } + + [Test] + public void Equals_And_GetHashCode_Work_ForSameAndDifferentPorts() + { + // arrange + var a = new UdpClientWrapper(12345); + var b = new UdpClientWrapper(12345); + var c = new UdpClientWrapper(54321); + + // act + assert + Assert.That(a.Equals(b), Is.True, "Wrapper-и з однаковим портом мають бути рівні"); + Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode()), + "HashCode для однакових параметрів має співпадати"); + + Assert.That(a.Equals(c), Is.False, "Wrapper-и з різними портами не повинні бути рівні"); + } +} + diff --git a/NetSdrClientAppTests/UdpClientWrapperEqualityTests.cs b/NetSdrClientAppTests/UdpClientWrapperEqualityTests.cs new file mode 100644 index 00000000..34a8ed9e --- /dev/null +++ b/NetSdrClientAppTests/UdpClientWrapperEqualityTests.cs @@ -0,0 +1,55 @@ +using System; +using NetSdrClientApp.Networking; +using NUnit.Framework; + +namespace NetSdrClientAppTests.Networking +{ + [TestFixture] + public class UdpClientWrapperEqualityTests + { + [Test] + public void Equals_SameReference_ReturnsTrue() + { + var wrapper = new UdpClientWrapper(60000); + + Assert.That(wrapper.Equals(wrapper), Is.True); + } + + [Test] + public void Equals_DifferentType_ReturnsFalse() + { + var wrapper = new UdpClientWrapper(60000); + + Assert.That(wrapper.Equals("not a wrapper"), Is.False); + } + + [Test] + public void Equals_And_GetHashCode_ForSameEndpoint_AreEqual() + { + var a = new UdpClientWrapper(60000); + var b = new UdpClientWrapper(60000); + + Assert.That(a.Equals(b), Is.True); + Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode())); + } + + [Test] + public void Equals_ForDifferentPorts_ReturnsFalse() + { + var a = new UdpClientWrapper(60000); + var b = new UdpClientWrapper(60001); + + Assert.That(a.Equals(b), Is.False); + } + + [Test] + public void StopWithoutStart_IsNoOp() + { + var wrapper = new UdpClientWrapper(60000); + + Assert.DoesNotThrow(() => wrapper.StopListening()); + Assert.DoesNotThrow(() => wrapper.Exit()); + } + } +} + From af38297a614479a4a11e5f34b9bdd51c8694c824 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 06:46:01 +0200 Subject: [PATCH 21/22] Lab6: Adding more tests --- .../TcpClientWrapperEqualityTests.cs | 38 -- NetSdrClientAppTests/TcpClientWrapperTests.cs | 324 ++++++++++++++++-- 2 files changed, 292 insertions(+), 70 deletions(-) delete mode 100644 NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs diff --git a/NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs b/NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs deleted file mode 100644 index a4598390..00000000 --- a/NetSdrClientAppTests/TcpClientWrapperEqualityTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Threading.Tasks; -using NetSdrClientApp.Networking; -using NUnit.Framework; - -namespace NetSdrClientAppTests.Networking; - -[TestFixture] -public class TcpClientWrapperDisposalTests -{ - [Test] - public void Disconnect_AfterDispose_ThrowsObjectDisposedException() - { - // arrange - var client = new TcpClientWrapper("127.0.0.1", 12345); - - // act - client.Dispose(); - - // assert - Assert.Throws(() => client.Disconnect()); - } - - [Test] - public void SendMessageAsync_AfterDispose_ThrowsObjectDisposedException() - { - // arrange - var client = new TcpClientWrapper("127.0.0.1", 12345); - - // act - client.Dispose(); - - // assert - Assert.ThrowsAsync(async () => - await client.SendMessageAsync("hello")); - } -} - diff --git a/NetSdrClientAppTests/TcpClientWrapperTests.cs b/NetSdrClientAppTests/TcpClientWrapperTests.cs index a6c839af..93eb2d4a 100644 --- a/NetSdrClientAppTests/TcpClientWrapperTests.cs +++ b/NetSdrClientAppTests/TcpClientWrapperTests.cs @@ -1,6 +1,9 @@ +using System; using System.Net; using System.Net.Sockets; +using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using NetSdrClientApp.Networking; using NUnit.Framework; @@ -10,62 +13,103 @@ namespace NetSdrClientAppTests.Networking [TestFixture] public class TcpClientWrapperTests { + private const string Host = "127.0.0.1"; - [Test] - public void Connect_WhenAlreadyConnected_DoesNotThrow_AndStaysConnected() - { - // arrange - var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; + // ----------------- helpers ----------------- - var wrapper = new TcpClientWrapper("127.0.0.1", port); + private static void SetPrivateField(TcpClientWrapper wrapper, string fieldName, T? value) + { + var field = typeof(TcpClientWrapper).GetField(fieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(field, $"Field '{fieldName}' was not found via reflection."); + field!.SetValue(wrapper, value); + } + + private static Task InvokeStartListeningAsync(TcpClientWrapper wrapper, CancellationToken token) + { + var method = typeof(TcpClientWrapper).GetMethod("StartListeningAsync", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(method, "StartListeningAsync not found via reflection."); + + var taskObj = method!.Invoke(wrapper, new object[] { token }); + return (Task)taskObj!; + } - // перше підключення – як у попередньому тесті - wrapper.Connect(); - Assert.IsTrue(wrapper.Connected, "Перше підключення не пройшло."); + /// + /// Стрім, який кидає в Close(), але мовчить у Dispose(). + /// Використовується, щоб зайти в catch у Disconnect. + /// + private sealed class ThrowOnCloseNetworkStream : NetworkStream + { + public ThrowOnCloseNetworkStream(Socket socket) + : base(socket, FileAccess.ReadWrite, ownsSocket: false) + { + } - // act + assert: другий виклик Connect() повинен піти в гілку - // `if (Connected) { Console.WriteLine("Already connected..."); return; }` - Assert.DoesNotThrow(() => wrapper.Connect(), "Повторний Connect не повинен кидати виняток."); - Assert.IsTrue(wrapper.Connected, "Після повторного Connect з’єднання має залишатися активним."); + public override void Close() + { + throw new InvalidOperationException("Close boom"); + } - // cleanup - wrapper.Disconnect(); - listener.Stop(); - } + protected override void Dispose(bool disposing) + { + // спеціально нічого не робимо, щоб finally у Disconnect не впав + } + } + + /// + /// Стрім, який кидає в ReadAsync, але мовчить у Dispose(). + /// Використовується, щоб зайти в catch (Exception ex) у StartListeningAsync. + /// + private sealed class ThrowOnReadNetworkStream : NetworkStream + { + public ThrowOnReadNetworkStream(Socket socket) + : base(socket, FileAccess.ReadWrite, ownsSocket: false) + { + } + public override bool CanRead => true; + + public override Task ReadAsync( + byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + throw new InvalidOperationException("ReadAsync boom"); + } + + protected override void Dispose(bool disposing) + { + // глушимо, щоб finally у StartListeningAsync завершився без винятку + } + } + + // ----------------- базові сценарії Connect / Send / Disconnect ----------------- [Test] - public void Disconnect_WhenNotConnected_DoesNotThrow() + public void Connect_WhenServerIsUnavailable_DoesNotThrow_AndStaysDisconnected() { - var wrapper = new TcpClientWrapper("localhost", 5555); + var wrapper = new TcpClientWrapper(Host, 1); - Assert.DoesNotThrow(() => wrapper.Disconnect()); - Assert.False(wrapper.Connected); + Assert.DoesNotThrow(() => wrapper.Connect()); + Assert.That(wrapper.Connected, Is.False); } [Test] public async Task Connect_And_SendMessage_WorksWithLocalListener() { - // arrange: піднімаємо локальний TcpListener на вільному порту var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; - var wrapper = new TcpClientWrapper("127.0.0.1", port); + var wrapper = new TcpClientWrapper(Host, port); - // приймаємо клієнта в бекґраунді var acceptTask = listener.AcceptTcpClientAsync(); - // act Assert.DoesNotThrow(() => wrapper.Connect()); - Assert.True(wrapper.Connected, "Wrapper should be connected after successful Connect()."); + Assert.True(wrapper.Connected, "Wrapper should be connected after Connect()."); - var message = "hello"; + const string message = "hello"; await wrapper.SendMessageAsync(message); - // assert: сервер реально щось отримав using var serverClient = await acceptTask; using var stream = serverClient.GetStream(); @@ -73,12 +117,228 @@ public async Task Connect_And_SendMessage_WorksWithLocalListener() var read = await stream.ReadAsync(buffer, 0, buffer.Length); var received = Encoding.UTF8.GetString(buffer, 0, read); - Assert.AreEqual(message, received); + Assert.That(received, Is.EqualTo(message)); + + wrapper.Disconnect(); + listener.Stop(); + } + + [Test] + public void Connect_WhenAlreadyConnected_DoesNotThrow_AndStaysConnected() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var wrapper = new TcpClientWrapper(Host, port); + var acceptTask = listener.AcceptTcpClientAsync(); + + wrapper.Connect(); + using var _ = acceptTask.Result; // лише для встановлення конекта + + Assert.IsTrue(wrapper.Connected, "First connect failed."); + + Assert.DoesNotThrow(() => wrapper.Connect()); + Assert.IsTrue(wrapper.Connected, "After second Connect() we still must be connected."); + + wrapper.Disconnect(); + listener.Stop(); + } + + [Test] + public void Disconnect_WhenNotConnected_DoesNotThrow() + { + var wrapper = new TcpClientWrapper("localhost", 5555); + + Assert.DoesNotThrow(() => wrapper.Disconnect()); + Assert.False(wrapper.Connected); + } + + [Test] + public async Task Dispose_WhenConnected_ReleasesResourcesAndDisconnects() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var acceptTask = listener.AcceptTcpClientAsync(); + var wrapper = new TcpClientWrapper(Host, port); + + wrapper.Connect(); + using var _ = await acceptTask; + + Assert.That(wrapper.Connected, Is.True); + + wrapper.Dispose(); + + Assert.That(wrapper.Connected, Is.False); + + listener.Stop(); + } + + [Test] + public void Dispose_CalledTwice_DoesNotThrow() + { + var wrapper = new TcpClientWrapper(Host, 12345); + + wrapper.Dispose(); + Assert.DoesNotThrow(() => wrapper.Dispose()); + } + + [Test] + public void SendMessageAsync_WhenNotConnected_ThrowsInvalidOperationException() + { + var wrapper = new TcpClientWrapper(Host, 65000); + + var ex = Assert.ThrowsAsync( + async () => await wrapper.SendMessageAsync("hello")); + + Assert.That(ex!.Message, Is.EqualTo("Not connected to a server.")); + } + + // ----------------- ThrowIfDisposed ----------------- + + [Test] + public void Connect_AfterDispose_ThrowsObjectDisposedException() + { + var wrapper = new TcpClientWrapper(Host, 12345); + wrapper.Dispose(); + + var ex = Assert.Throws(() => wrapper.Connect()); + Assert.That(ex!.ObjectName, Is.EqualTo(nameof(TcpClientWrapper))); + } + + [Test] + public void Disconnect_AfterDispose_ThrowsObjectDisposedException() + { + var wrapper = new TcpClientWrapper(Host, 12345); + wrapper.Dispose(); + + Assert.Throws(() => wrapper.Disconnect()); + } + + [Test] + public void SendMessageAsync_AfterDispose_ThrowsObjectDisposedException() + { + var wrapper = new TcpClientWrapper(Host, 12345); + wrapper.Dispose(); + + Assert.ThrowsAsync( + async () => await wrapper.SendMessageAsync("hello")); + } + + // ----------------- Connect: стара CTS ----------------- + + [Test] + public void Connect_WhenOldCancellationTokenSourceExists_CancelsAndDisposesIt() + { + var wrapper = new TcpClientWrapper(Host, 1); + + var oldCts = new CancellationTokenSource(); + SetPrivateField(wrapper, "_cts", oldCts); + + Assert.DoesNotThrow(() => wrapper.Connect()); + + Assert.That(oldCts.IsCancellationRequested, Is.True, + "Old CTS must be cancelled when Connect() is called again."); + } + + // ----------------- Disconnect: catch (Exception ex) ----------------- + + [Test] + public async Task Disconnect_WhenStreamCloseThrows_ThrowsInvalidOperationException() + { + // реальне з’єднання, щоб TcpClient.Connected == true + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var client = new TcpClient(); + var acceptTask = listener.AcceptTcpClientAsync(); + await client.ConnectAsync(Host, port); + using var _ = await acceptTask; // тримаємо серверний бік відкритим + + var wrapper = new TcpClientWrapper("dummy", 0); + + var throwingStream = new ThrowOnCloseNetworkStream(client.Client); + + SetPrivateField(wrapper, "_tcpClient", client); + SetPrivateField(wrapper, "_stream", throwingStream); + SetPrivateField(wrapper, "_cts", new CancellationTokenSource()); + + Assert.That(wrapper.Connected, Is.True, "Precondition: wrapper must be logically connected."); + + var ex = Assert.Throws(() => wrapper.Disconnect()); + Assert.That(ex!.Message, Is.EqualTo("Close boom")); + + listener.Stop(); + } + + // ----------------- StartListeningAsync: успішний сценарій (MessageReceived) ----------------- + + [Test] + public async Task MessageReceived_Event_Raised_WhenServerSendsData() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var wrapper = new TcpClientWrapper(Host, port); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + wrapper.MessageReceived += (sender, data) => + { + tcs.TrySetResult(data); + }; + + var acceptTask = listener.AcceptTcpClientAsync(); + + // Connect() всередині запустить StartListeningAsync(_cts.Token) + wrapper.Connect(); + + using var serverClient = await acceptTask; + using var serverStream = serverClient.GetStream(); + + var msgBytes = Encoding.UTF8.GetBytes("ping"); + await serverStream.WriteAsync(msgBytes, 0, msgBytes.Length); + + var completed = await Task.WhenAny(tcs.Task, Task.Delay(2000)); + Assert.That(completed, Is.SameAs(tcs.Task), "MessageReceived was not raised in time."); + + var received = tcs.Task.Result; + Assert.That(Encoding.UTF8.GetString(received), Is.EqualTo("ping")); - // cleanup wrapper.Disconnect(); listener.Stop(); } + + // ----------------- StartListeningAsync: catch (Exception ex) ----------------- + + [Test] + public async Task StartListeningAsync_WhenReadAsyncThrows_DoesNotPropagateException() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var client = new TcpClient(); + var acceptTask = listener.AcceptTcpClientAsync(); + await client.ConnectAsync(Host, port); + using var _ = await acceptTask; + + var wrapper = new TcpClientWrapper("dummy", 0); + + var throwingStream = new ThrowOnReadNetworkStream(client.Client); + + SetPrivateField(wrapper, "_tcpClient", client); + SetPrivateField(wrapper, "_stream", throwingStream); + + var listenTask = InvokeStartListeningAsync(wrapper, CancellationToken.None); + + await listenTask; + + listener.Stop(); + } } } From 933e0d725972718d5786b89324c7751b14b92770 Mon Sep 17 00:00:00 2001 From: Yura Kozyr <9341915@stud.kai.edu.ua> Date: Wed, 3 Dec 2025 08:25:52 +0200 Subject: [PATCH 22/22] Update Newtonsoft.Json and SharpZipLib to secure versions --- .github/dependabot.yml | 6 ++++++ NetSdrClientApp/NetSdrClientApp.csproj | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..446b951e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj index 2ac91006..cb3674d8 100644 --- a/NetSdrClientApp/NetSdrClientApp.csproj +++ b/NetSdrClientApp/NetSdrClientApp.csproj @@ -7,8 +7,8 @@ enable - - + +