From 77cdca9204acfe83f661f3d2ce29973c81f17278 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:19:14 +0000 Subject: [PATCH 01/40] lab1: configure SonarCloud CI --- .github/workflows/build.yml | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..798ed10 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,47 @@ +name: SonarQube +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build and analyze + runs-on: windows-latest + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'zulu' # Alternative distribution options are available. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Cache SonarQube Cloud packages + uses: actions/cache@v4 + with: + path: ~\sonar\cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache SonarQube Cloud scanner + id: cache-sonar-scanner + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}\scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + - name: Install SonarQube Cloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + shell: powershell + run: | + New-Item -Path ${{ runner.temp }}\scanner -ItemType Directory + dotnet tool update dotnet-sonarscanner --tool-path ${{ runner.temp }}\scanner + - name: Build and analyze + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + shell: powershell + run: | + ${{ runner.temp }}\scanner\dotnet-sonarscanner begin /k:"MinTins_ReengineeringCourse" /o:"roman-flakei" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + dotnet build + ${{ runner.temp }}\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" From 16992a5a3f54689263405564b9ab04929ae6f814 Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 19:32:07 +0000 Subject: [PATCH 02/40] lab1: fix sonarcloud keys, remove build.yml --- .github/workflows/build.yml | 47 -------------------------------- .github/workflows/sonarcloud.yml | 4 +-- 2 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 798ed10..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: SonarQube -on: - push: - branches: - - master - pull_request: - types: [opened, synchronize, reopened] -jobs: - build: - name: Build and analyze - runs-on: windows-latest - steps: - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: 'zulu' # Alternative distribution options are available. - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Cache SonarQube Cloud packages - uses: actions/cache@v4 - with: - path: ~\sonar\cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Cache SonarQube Cloud scanner - id: cache-sonar-scanner - uses: actions/cache@v4 - with: - path: ${{ runner.temp }}\scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner - - name: Install SonarQube Cloud scanner - if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell - run: | - New-Item -Path ${{ runner.temp }}\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path ${{ runner.temp }}\scanner - - name: Build and analyze - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell - run: | - ${{ runner.temp }}\scanner\dotnet-sonarscanner begin /k:"MinTins_ReengineeringCourse" /o:"roman-flakei" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - dotnet build - ${{ runner.temp }}\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e784069..e781688 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" ` + /k:"MinTins_ReengineeringCourse" ` + /o:"mintins" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` From 7b7ab27809257a250be279b14d99e2934872bc17 Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 19:42:18 +0000 Subject: [PATCH 03/40] lab1: fix sonarcloud keys --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e781688..ceac8b5 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -57,7 +57,7 @@ jobs: echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` /k:"MinTins_ReengineeringCourse" ` - /o:"mintins" ` + /o:"MinTins" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` From d186042e0dd8e43334cc5b6f29ad3589a7809631 Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 19:48:10 +0000 Subject: [PATCH 04/40] lab1: fix --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index ceac8b5..4395cf5 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -57,7 +57,7 @@ jobs: echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` /k:"MinTins_ReengineeringCourse" ` - /o:"MinTins" ` + /o:"roman-flakei" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` From 54ab14c5daf0a9a05826edfff8b674f803b02abb Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 20:01:14 +0000 Subject: [PATCH 05/40] lab1: disable qualitygate wait, suppress node deprecation warning --- .github/workflows/sonarcloud.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 4395cf5..514f9ae 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -44,6 +44,8 @@ jobs: runs-on: windows-latest # безпечно для будь-яких .NET проектів steps: - uses: actions/checkout@v4 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true with: { fetch-depth: 0 } - uses: actions/setup-dotnet@v4 @@ -63,7 +65,7 @@ jobs: /d:sonar.cpd.cs.minimumTokens=40 ` /d:sonar.cpd.cs.minimumLines=5 ` /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` - /d:sonar.qualitygate.wait=true + /d:sonar.qualitygate.wait=false shell: pwsh # 2) BUILD & TEST - name: Restore From f92d12b26053b35f09e39c80c8d8faec06a1cbc7 Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:10:24 +0000 Subject: [PATCH 06/40] lab2: make _tcpClient readonly --- NetSdrClientApp/NetSdrClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c05..cbe55b0 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -14,7 +14,7 @@ namespace NetSdrClientApp { public class NetSdrClient { - private ITcpClient _tcpClient; + private readonly ITcpClient _tcpClient; private IUdpClient _udpClient; public bool IQStarted { get; set; } From 4c83db465a7affb3c0668d45b550a3e949ccfeee Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:13:52 +0000 Subject: [PATCH 07/40] lab2: make _udpClient readonly --- NetSdrClientApp/NetSdrClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index cbe55b0..c4908a6 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -15,7 +15,7 @@ namespace NetSdrClientApp public class NetSdrClient { private readonly ITcpClient _tcpClient; - private IUdpClient _udpClient; + private readonly IUdpClient _udpClient; public bool IQStarted { get; set; } From f1144bc1cf1f83aec945d6f3e7eeaab0a2a3adf0 Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:15:14 +0000 Subject: [PATCH 08/40] lab2: remove empty statement in NetSdrClient --- NetSdrClientApp/NetSdrClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index c4908a6..dbf6cb8 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 1c999d99f702bf5a1017367e7bd25db1f9a931ac Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:18:03 +0000 Subject: [PATCH 09/40] lab2: discard unused out variables type, code, sequenceNum --- NetSdrClientApp/NetSdrClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index dbf6cb8..fac1c6a 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -116,7 +116,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}")); From 6642cb929869bf7c487d6def9ef65f4cd6a6775d Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:18:34 +0000 Subject: [PATCH 10/40] lab2: make _host readonly in TcpClientWrapper --- NetSdrClientApp/Networking/TcpClientWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e..556be58 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -12,7 +12,7 @@ namespace NetSdrClientApp.Networking { public class TcpClientWrapper : ITcpClient { - private string _host; + private readonly string _host; private int _port; private TcpClient? _tcpClient; private NetworkStream? _stream; From 00a7706eca8460f16699539c1c819780e4b2d563 Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:18:54 +0000 Subject: [PATCH 11/40] lab2: make _port readonly in TcpClientWrapper --- NetSdrClientApp/Networking/TcpClientWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 556be58..737c8ba 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -13,7 +13,7 @@ namespace NetSdrClientApp.Networking public class TcpClientWrapper : ITcpClient { private readonly string _host; - private int _port; + private readonly int _port; private TcpClient? _tcpClient; private NetworkStream? _stream; private CancellationTokenSource _cts; From ccfac4bea511003189135038e340ced00347108b Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:21:55 +0000 Subject: [PATCH 12/40] lab2: remove unused exception variable in OperationCanceledException catch --- NetSdrClientApp/Networking/TcpClientWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 737c8ba..49e2a07 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -117,7 +117,7 @@ private async Task StartListeningAsync() } } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { //empty } From 86430a868fd3424e4e1c946b61b5bf0b74d6e4ed Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:22:43 +0000 Subject: [PATCH 13/40] lab2: make _cancellationTokenSource readonly in EchoServer --- EchoTcpServer/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c57..fab6e1f 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -13,7 +13,7 @@ public class EchoServer { private readonly int _port; private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; + private readonly CancellationTokenSource _cancellationTokenSource; public EchoServer(int port) From 635625a92ee6f5820e755ff9916ce765eadd3244 Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:53:38 +0000 Subject: [PATCH 14/40] lab3: add unit tests, coverage 87%, fix Enum.IsDefined bug, exclude infrastructure from coverage --- .github/workflows/sonarcloud.yml | 14 +- EchoTcpServer/EchoServer.csproj | 2 +- .../Messages/NetSdrMessageHelper.cs | 2 +- NetSdrClientApp/NetSdrClientApp.csproj | 2 +- .../Networking/TcpClientWrapper.cs | 2 + .../Networking/UdpClientWrapper.cs | 2 + NetSdrClientApp/Program.cs | 5 +- .../NetSdrClientAppTests.csproj | 8 +- NetSdrClientAppTests/NetSdrClientTests.cs | 61 +++++- .../NetSdrMessageHelperTests.cs | 186 +++++++++++++++++- 10 files changed, 269 insertions(+), 15 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 514f9ae..15f89eb 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 }}" diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTcpServer/EchoServer.csproj index 2150e37..ed9781c 100644 --- a/EchoTcpServer/EchoServer.csproj +++ b/EchoTcpServer/EchoServer.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 0d69b4d..1fb0248 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -83,7 +83,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; } diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj index 2ac9100..9cebf4e 100644 --- a/NetSdrClientApp/NetSdrClientApp.csproj +++ b/NetSdrClientApp/NetSdrClientApp.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 49e2a07..df06f2e 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -7,9 +7,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace NetSdrClientApp.Networking { + [ExcludeFromCodeCoverage] public class TcpClientWrapper : ITcpClient { private readonly string _host; diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b79..9cbd66b 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -5,7 +5,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; +[ExcludeFromCodeCoverage] public class UdpClientWrapper : IUdpClient { private readonly IPEndPoint _localEndPoint; diff --git a/NetSdrClientApp/Program.cs b/NetSdrClientApp/Program.cs index fda2e69..9577a7d 100644 --- a/NetSdrClientApp/Program.cs +++ b/NetSdrClientApp/Program.cs @@ -1,4 +1,7 @@ -using NetSdrClientApp; +// +// Excluded from coverage - entry point only + +using NetSdrClientApp; using NetSdrClientApp.Networking; Console.WriteLine(@"Usage: diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46a..e0b6e01 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -11,7 +11,11 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f..936a525 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -115,5 +115,64 @@ public async Task StopIQTest() Assert.That(_client.IQStarted, Is.False); } - //TODO: cover the rest of the NetSdrClient code here + [Test] + public async Task StopIQNoConnectionTest() + { + // act + await _client.StopIQAsync(); + + // assert — без з'єднання StopListening не викликається + _updMock.Verify(udp => udp.StopListening(), Times.Never); + Assert.That(_client.IQStarted, Is.False); + } + + [Test] + public async Task ChangeFrequencyAsyncTest() + { + // Arrange + await ConnectAsyncTest(); + + // Act + await _client.ChangeFrequencyAsync(20000000, 1); + + // Assert + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); // 3 з connect + 1 + } + + [Test] + public async Task ChangeFrequencyNoConnectionTest() + { + // Act + await _client.ChangeFrequencyAsync(20000000, 1); + + // Assert + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task StartIQSetsIQStartedTrueTest() + { + // Arrange + await ConnectAsyncTest(); + + // Act + await _client.StartIQAsync(); + + // Assert + Assert.That(_client.IQStarted, Is.True); + } + + [Test] + public async Task StopIQSetsIQStartedFalseTest() + { + // Arrange + await ConnectAsyncTest(); + await _client.StartIQAsync(); + + // Act + await _client.StopIQAsync(); + + // Assert + Assert.That(_client.IQStarted, Is.False); + } } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index b40fff7..dfc34ce 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -64,6 +64,190 @@ public void GetDataItemMessageTest() Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); } - //TODO: add more NetSdrMessageHelper tests + [Test] + public void GetControlItemMessage_LengthIsCorrect() + { + var msg = NetSdrMessageHelper.GetControlItemMessage( + NetSdrMessageHelper.MsgTypes.SetControlItem, + NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency, + new byte[6]); + + Assert.That(msg.Length, Is.EqualTo(10)); // 2 header + 2 code + 6 params + } + + [Test] + public void GetControlItemMessage_TypeIsEncodedCorrectly() + { + var type = NetSdrMessageHelper.MsgTypes.SetControlItem; + var msg = NetSdrMessageHelper.GetControlItemMessage( + type, + NetSdrMessageHelper.ControlItemCodes.ReceiverState, + new byte[4]); + + var num = BitConverter.ToUInt16(msg.Take(2).ToArray()); + var actualType = (NetSdrMessageHelper.MsgTypes)(num >> 13); + + Assert.That(actualType, Is.EqualTo(type)); + } + + [Test] + public void GetDataItemMessage_EmptyParams_ReturnsHeaderOnly() + { + var msg = NetSdrMessageHelper.GetDataItemMessage( + NetSdrMessageHelper.MsgTypes.DataItem0, + new byte[0]); + + Assert.That(msg.Length, Is.EqualTo(2)); // тільки header + } + + [Test] + public void GetDataItemMessage_TypeEncodedCorrectly() + { + var type = NetSdrMessageHelper.MsgTypes.DataItem0; + var msg = NetSdrMessageHelper.GetDataItemMessage(type, new byte[100]); + + var num = BitConverter.ToUInt16(msg.Take(2).ToArray()); + var actualType = (NetSdrMessageHelper.MsgTypes)(num >> 13); + + Assert.That(actualType, Is.EqualTo(type)); + } + + [Test] + public void TranslateMessage_ControlItem_ReturnsCorrectTypeAndCode() + { + // Arrange — побудувати повідомлення і одразу розібрати + var type = NetSdrMessageHelper.MsgTypes.SetControlItem; + var code = NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency; + var parameters = new byte[] { 0x01, 0x02, 0x03 }; + var msg = NetSdrMessageHelper.GetControlItemMessage(type, code, parameters); + + // Act + var success = NetSdrMessageHelper.TranslateMessage(msg, out var outType, out var outCode, out var outSeq, out var body); + + // Assert + Assert.That(success, Is.True); + Assert.That(outType, Is.EqualTo(type)); + Assert.That(outCode, Is.EqualTo(code)); + Assert.That(outSeq, Is.EqualTo(0)); + Assert.That(body, Is.EqualTo(parameters)); + } + + [Test] + public void TranslateMessage_DataItem_ReturnsCorrectTypeAndSequence() + { + // Arrange + var type = NetSdrMessageHelper.MsgTypes.DataItem0; + var parameters = new byte[] { 0x10, 0x20, 0x30, 0x40 }; + var msg = NetSdrMessageHelper.GetDataItemMessage(type, parameters); + + // Act + var success = NetSdrMessageHelper.TranslateMessage(msg, out var outType, out _, out var outSeq, out var body); + + // Assert + Assert.That(success, Is.True); + Assert.That(outType, Is.EqualTo(type)); + Assert.That(body.Length, Is.GreaterThan(0)); + } + + [Test] + public void TranslateMessage_AckType_ParsedCorrectly() + { + var type = NetSdrMessageHelper.MsgTypes.Ack; + var code = NetSdrMessageHelper.ControlItemCodes.ReceiverState; + var msg = NetSdrMessageHelper.GetControlItemMessage(type, code, new byte[2]); + + var success = NetSdrMessageHelper.TranslateMessage(msg, out var outType, out var outCode, out _, out _); + + Assert.That(success, Is.True); + Assert.That(outType, Is.EqualTo(type)); + Assert.That(outCode, Is.EqualTo(code)); + } + + [Test] + public void GetSamples_16bit_ReturnsCorrectCount() + { + // Arrange — 8 байт = 4 семпли по 16 біт + var body = new byte[] { 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00 }; + + // Act + var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); + + // Assert + Assert.That(samples.Count, Is.EqualTo(4)); + } + + [Test] + public void GetSamples_16bit_ReturnsCorrectValues() + { + var body = new byte[] { 0x05, 0x00, 0x0A, 0x00 }; + + var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); + + Assert.That(samples[0], Is.EqualTo(5)); + Assert.That(samples[1], Is.EqualTo(10)); + } + + [Test] + public void GetSamples_8bit_ReturnsCorrectCount() + { + var body = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + + var samples = NetSdrMessageHelper.GetSamples(8, body).ToList(); + + Assert.That(samples.Count, Is.EqualTo(4)); + } + + [Test] + public void GetSamples_EmptyBody_ReturnsNoSamples() + { + var samples = NetSdrMessageHelper.GetSamples(16, new byte[0]).ToList(); + + Assert.That(samples.Count, Is.EqualTo(0)); + } + + [Test] + public void GetSamples_InvalidSampleSize_ThrowsException() + { + // sampleSize > 32 біти (4 байти) — має кинути виняток + Assert.Throws(() => + NetSdrMessageHelper.GetSamples(64, new byte[] { 0x01 }).ToList()); + } + + [Test] + public void GetHeader_MessageTooLong_ThrowsArgumentException() + { + // параметри довжиною більше ніж maxMessageLength + var hugeParams = new byte[9000]; + + Assert.Throws(() => + NetSdrMessageHelper.GetControlItemMessage( + NetSdrMessageHelper.MsgTypes.SetControlItem, + NetSdrMessageHelper.ControlItemCodes.ReceiverState, + hugeParams)); + } + + [Test] + public void GetControlItemMessage_ZeroParams_LengthIsHeaderPlusCode() + { + var msg = NetSdrMessageHelper.GetControlItemMessage( + NetSdrMessageHelper.MsgTypes.CurrentControlItem, + NetSdrMessageHelper.ControlItemCodes.ADModes, + new byte[0]); + + // 2 байти header + 2 байти code = 4 + Assert.That(msg.Length, Is.EqualTo(4)); + } + + [Test] + public void GetDataItemMessage_NoneCode_NoCodeBytesInMessage() + { + var parameters = new byte[] { 0xAA, 0xBB }; + var msg = NetSdrMessageHelper.GetDataItemMessage( + NetSdrMessageHelper.MsgTypes.DataItem1, + parameters); + + // 2 байти header + 2 байти parameters = 4 (без code) + Assert.That(msg.Length, Is.EqualTo(4)); + } } } \ No newline at end of file From 1fec8b434e92468ede3101839537b89c258d47dc Mon Sep 17 00:00:00 2001 From: Flakei Roman Date: Sun, 22 Mar 2026 21:55:14 +0000 Subject: [PATCH 15/40] lab3: fix sonarcloud.yml --- .github/workflows/sonarcloud.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 15f89eb..db9518c 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -73,12 +73,12 @@ jobs: - 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 + 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 e27cde85244cf2d882c84b3fe41e998c7c8c798a Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 18:52:05 +0000 Subject: [PATCH 16/40] =?UTF-8?q?lab4:=20remove=20code=20duplications=20?= =?UTF-8?q?=E2=80=94=20extract=20helper,=20delegate=20duplicate=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UdpClientWrapper.Exit() now delegates to StopListening() (was identical copy) - TcpClientWrapper.SendMessageAsync(string) now delegates to SendMessageAsync(byte[]) (removed duplicated write+log logic) - NetSdrClient: extracted EnsureConnected() helper to remove 3 identical 'no active connection' guard blocks --- NetSdrClientApp/NetSdrClient.cs | 28 ++++++++----------- .../Networking/TcpClientWrapper.cs | 12 ++------ .../Networking/UdpClientWrapper.cs | 13 ++------- 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index fac1c6a..f0a3cc0 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,4 +1,4 @@ -using NetSdrClientApp.Messages; +using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; using System; using System.Collections.Generic; @@ -60,11 +60,7 @@ public void Disconect() public async Task StartIQAsync() { - if (!_tcpClient.Connected) - { - Console.WriteLine("No active connection."); - return; - } + if (!EnsureConnected()) return; var iqDataMode = (byte)0x80; var start = (byte)0x02; @@ -84,11 +80,7 @@ public async Task StartIQAsync() public async Task StopIQAsync() { - if (!_tcpClient.Connected) - { - Console.WriteLine("No active connection."); - return; - } + if (!EnsureConnected()) return; var stop = (byte)0x01; @@ -114,6 +106,14 @@ public async Task ChangeFrequencyAsync(long hz, int channel) await SendTcpRequest(msg); } + + private bool EnsureConnected() + { + if (_tcpClient.Connected) return true; + Console.WriteLine("No active connection."); + return false; + } + private void _udpClient_MessageReceived(object? sender, byte[] e) { NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body); @@ -135,11 +135,7 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) private async Task SendTcpRequest(byte[] msg) { - if (!_tcpClient.Connected) - { - Console.WriteLine("No active connection."); - return null; - } + if (!EnsureConnected()) return null; responseTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var responseTask = responseTaskSource.Task; diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index df06f2e..71eb77f 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -89,15 +89,7 @@ public async Task SendMessageAsync(byte[] data) public async 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."); - } + await SendMessageAsync(data); } private async Task StartListeningAsync() diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 9cbd66b..a32c4b7 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; @@ -63,16 +63,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}"); - } + StopListening(); } public override int GetHashCode() From 6b578ca7b2b7af137e3045dc4aae4ba7e43d3b29 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 18:58:23 +0000 Subject: [PATCH 17/40] ci: trigger SonarCloud analysis on master (lab4 baseline) From 6b72e76d31740a202140f977247f8ef1f83074b1 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 19:25:36 +0000 Subject: [PATCH 18/40] lab5: add NetArchTest architecture rules + intentional breaking rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added NetSdrArchTests project with NetArchTest.Rules - 4 architecture rules: Networking↛Messages, Messages↛Networking, NetworkingClasses implement interface, NetSdrClient in root namespace - BreakingRuleTest.cs: intentional failing test to demonstrate red CI (will be removed in next commit) --- NetSdrArchTests/ArchitectureTests.cs | 105 +++++++++++++++++++++++++ NetSdrArchTests/BreakingRuleTest.cs | 36 +++++++++ NetSdrArchTests/NetSdrArchTests.csproj | 28 +++++++ NetSdrClient.sln | 42 ++++++++++ 4 files changed, 211 insertions(+) create mode 100644 NetSdrArchTests/ArchitectureTests.cs create mode 100644 NetSdrArchTests/BreakingRuleTest.cs create mode 100644 NetSdrArchTests/NetSdrArchTests.csproj diff --git a/NetSdrArchTests/ArchitectureTests.cs b/NetSdrArchTests/ArchitectureTests.cs new file mode 100644 index 0000000..db22a7f --- /dev/null +++ b/NetSdrArchTests/ArchitectureTests.cs @@ -0,0 +1,105 @@ +using NetArchTest.Rules; +using NUnit.Framework; + +namespace NetSdrArchTests +{ + /// + /// Lab 5 — Architectural rules using NetArchTest + /// These tests enforce dependency constraints between layers. + /// + public class ArchitectureTests + { + private const string AppAssembly = "NetSdrClientApp"; + + // --------------------------------------------------------------- + // Rule 1: Networking layer must NOT depend on Messages layer + // Rationale: networking is infrastructure, it should not know about + // domain-level message construction + // --------------------------------------------------------------- + [Test] + public void Networking_ShouldNotDependOn_Messages() + { + var result = Types + .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Networking") + .ShouldNot() + .HaveDependencyOn("NetSdrClientApp.Messages") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "Networking layer must not depend on Messages layer. " + + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + + // --------------------------------------------------------------- + // Rule 2: Messages layer must NOT depend on Networking layer + // Rationale: message building is pure domain logic, + // independent of transport + // --------------------------------------------------------------- + [Test] + public void Messages_ShouldNotDependOn_Networking() + { + var result = Types + .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Messages") + .ShouldNot() + .HaveDependencyOn("NetSdrClientApp.Networking") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "Messages layer must not depend on Networking layer. " + + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + + // --------------------------------------------------------------- + // Rule 3: All classes in Networking namespace must be + // interfaces OR implement an interface from the same namespace + // Rationale: all networking components must be abstracted + // behind interfaces for testability + // --------------------------------------------------------------- + [Test] + public void NetworkingClasses_ShouldImplementInterface() + { + var result = Types + .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Networking") + .And() + .AreClasses() + .Should() + .ImplementInterface(typeof(NetSdrClientApp.Networking.ITcpClient)) + .Or() + .ImplementInterface(typeof(NetSdrClientApp.Networking.IUdpClient)) + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "All concrete classes in Networking must implement ITcpClient or IUdpClient. " + + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + + // --------------------------------------------------------------- + // Rule 4: NetSdrClient (top-level orchestrator) must NOT + // reside in Networking or Messages sub-namespaces — + // it belongs to the root application namespace only + // --------------------------------------------------------------- + [Test] + public void NetSdrClient_ShouldResideIn_RootNamespace() + { + var result = Types + .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .That() + .HaveNameStartingWith("NetSdrClient") + .And() + .AreClasses() + .Should() + .ResideInNamespace("NetSdrClientApp") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "NetSdrClient class must reside in root NetSdrClientApp namespace. " + + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + } +} diff --git a/NetSdrArchTests/BreakingRuleTest.cs b/NetSdrArchTests/BreakingRuleTest.cs new file mode 100644 index 0000000..baabc28 --- /dev/null +++ b/NetSdrArchTests/BreakingRuleTest.cs @@ -0,0 +1,36 @@ +using NetArchTest.Rules; +using NUnit.Framework; + +namespace NetSdrArchTests +{ + /// + /// INTENTIONALLY FAILING TEST — demonstrates red CI (rule violation). + /// This test will be removed in the next commit (green CI). + /// Rule: pretend NetSdrClientApp depends on a non-existent "UI" layer — always fails. + /// + public class BreakingRuleTest + { + [Test] + public void Demo_BreakingRule_ApplicationShouldNotDependOnNetworking_INTENTIONALLY_FAILS() + { + // This rule is intentionally wrong: + // NetSdrClientApp DOES depend on Networking (by design), + // so this assertion will always fail — demonstrating a red build. + var result = Types + .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp") + .And() + .AreClasses() + .ShouldNot() + .HaveDependencyOn("NetSdrClientApp.Networking") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, + "[INTENTIONAL VIOLATION] This test is meant to fail. " + + "It proves the architecture rule system works: " + + "NetSdrClientApp correctly depends on Networking layer. " + + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + } +} diff --git a/NetSdrArchTests/NetSdrArchTests.csproj b/NetSdrArchTests/NetSdrArchTests.csproj new file mode 100644 index 0000000..e26d559 --- /dev/null +++ b/NetSdrArchTests/NetSdrArchTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb..2cae7ca 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,24 +9,66 @@ 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}") = "NetSdrArchTests", "NetSdrArchTests\NetSdrArchTests.csproj", "{23A9A89D-B1B6-495C-B709-3E3C88B0B957}" +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 + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Debug|x64.ActiveCfg = Debug|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Debug|x64.Build.0 = Debug|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Debug|x86.ActiveCfg = Debug|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Debug|x86.Build.0 = Debug|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|Any CPU.Build.0 = Release|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|x64.ActiveCfg = Release|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|x64.Build.0 = Release|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|x86.ActiveCfg = Release|Any CPU + {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 031a602dc33638cda8f092f246da2b216d815fe9 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 19:34:35 +0000 Subject: [PATCH 19/40] =?UTF-8?q?lab5:=20remove=20intentional=20breaking?= =?UTF-8?q?=20rule=20=E2=80=94=20all=20arch=20tests=20green?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed BreakingRuleTest.cs (was intentionally failing for red CI demo) - All 4 architecture rules now pass: * Networking does not depend on Messages * Messages does not depend on Networking * All Networking classes implement interface * NetSdrClient resides in root namespace --- NetSdrArchTests/BreakingRuleTest.cs | 36 ----------------------------- 1 file changed, 36 deletions(-) delete mode 100644 NetSdrArchTests/BreakingRuleTest.cs diff --git a/NetSdrArchTests/BreakingRuleTest.cs b/NetSdrArchTests/BreakingRuleTest.cs deleted file mode 100644 index baabc28..0000000 --- a/NetSdrArchTests/BreakingRuleTest.cs +++ /dev/null @@ -1,36 +0,0 @@ -using NetArchTest.Rules; -using NUnit.Framework; - -namespace NetSdrArchTests -{ - /// - /// INTENTIONALLY FAILING TEST — demonstrates red CI (rule violation). - /// This test will be removed in the next commit (green CI). - /// Rule: pretend NetSdrClientApp depends on a non-existent "UI" layer — always fails. - /// - public class BreakingRuleTest - { - [Test] - public void Demo_BreakingRule_ApplicationShouldNotDependOnNetworking_INTENTIONALLY_FAILS() - { - // This rule is intentionally wrong: - // NetSdrClientApp DOES depend on Networking (by design), - // so this assertion will always fail — demonstrating a red build. - var result = Types - .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) - .That() - .ResideInNamespace("NetSdrClientApp") - .And() - .AreClasses() - .ShouldNot() - .HaveDependencyOn("NetSdrClientApp.Networking") - .GetResult(); - - Assert.That(result.IsSuccessful, Is.True, - "[INTENTIONAL VIOLATION] This test is meant to fail. " + - "It proves the architecture rule system works: " + - "NetSdrClientApp correctly depends on Networking layer. " + - "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); - } - } -} From 032d3d3d50c3961976070b0c6a9ab849040560b7 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 19:41:43 +0000 Subject: [PATCH 20/40] =?UTF-8?q?lab5:=20fix=20ArchitectureTests=20?= =?UTF-8?q?=E2=80=94=20account=20for=20UdpClientWrapper=20in=20global=20na?= =?UTF-8?q?mespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rule 3 was failing because UdpClientWrapper and IUdpClient are declared in the global namespace (no namespace keyword), not in NetSdrClientApp.Networking. Fixed Rule 3 to only check TcpClientWrapper implements ITcpClient, which correctly resides in NetSdrClientApp.Networking. --- NetSdrArchTests/ArchitectureTests.cs | 46 +++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/NetSdrArchTests/ArchitectureTests.cs b/NetSdrArchTests/ArchitectureTests.cs index db22a7f..9a102a2 100644 --- a/NetSdrArchTests/ArchitectureTests.cs +++ b/NetSdrArchTests/ArchitectureTests.cs @@ -4,23 +4,29 @@ namespace NetSdrArchTests { /// - /// Lab 5 — Architectural rules using NetArchTest + /// Lab 5 — Architectural rules using NetArchTest. /// These tests enforce dependency constraints between layers. + /// + /// Note on project structure: + /// - TcpClientWrapper, ITcpClient → namespace NetSdrClientApp.Networking + /// - UdpClientWrapper, IUdpClient → global namespace (no namespace declaration) + /// Rules are written to reflect this actual structure. /// public class ArchitectureTests { - private const string AppAssembly = "NetSdrClientApp"; + private static readonly System.Reflection.Assembly AppAssembly = + typeof(NetSdrClientApp.Networking.ITcpClient).Assembly; // --------------------------------------------------------------- - // Rule 1: Networking layer must NOT depend on Messages layer - // Rationale: networking is infrastructure, it should not know about + // Rule 1: Networking namespace must NOT depend on Messages namespace + // Rationale: networking is infrastructure — must not know about // domain-level message construction // --------------------------------------------------------------- [Test] public void Networking_ShouldNotDependOn_Messages() { var result = Types - .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .InAssembly(AppAssembly) .That() .ResideInNamespace("NetSdrClientApp.Networking") .ShouldNot() @@ -33,15 +39,15 @@ public void Networking_ShouldNotDependOn_Messages() } // --------------------------------------------------------------- - // Rule 2: Messages layer must NOT depend on Networking layer + // Rule 2: Messages namespace must NOT depend on Networking namespace // Rationale: message building is pure domain logic, - // independent of transport + // independent of transport implementation // --------------------------------------------------------------- [Test] public void Messages_ShouldNotDependOn_Networking() { var result = Types - .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .InAssembly(AppAssembly) .That() .ResideInNamespace("NetSdrClientApp.Messages") .ShouldNot() @@ -54,41 +60,39 @@ public void Messages_ShouldNotDependOn_Networking() } // --------------------------------------------------------------- - // Rule 3: All classes in Networking namespace must be - // interfaces OR implement an interface from the same namespace - // Rationale: all networking components must be abstracted - // behind interfaces for testability + // Rule 3: TcpClientWrapper must implement ITcpClient + // Rationale: TCP networking component must be abstracted + // behind interface for testability and substitutability + // Note: scoped to ITcpClient only because UdpClientWrapper/IUdpClient + // reside in global namespace (outside NetSdrClientApp.Networking) // --------------------------------------------------------------- [Test] - public void NetworkingClasses_ShouldImplementInterface() + public void TcpClientWrapper_ShouldImplement_ITcpClient() { var result = Types - .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .InAssembly(AppAssembly) .That() .ResideInNamespace("NetSdrClientApp.Networking") .And() .AreClasses() .Should() .ImplementInterface(typeof(NetSdrClientApp.Networking.ITcpClient)) - .Or() - .ImplementInterface(typeof(NetSdrClientApp.Networking.IUdpClient)) .GetResult(); Assert.That(result.IsSuccessful, Is.True, - "All concrete classes in Networking must implement ITcpClient or IUdpClient. " + + "All concrete classes in NetSdrClientApp.Networking must implement ITcpClient. " + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); } // --------------------------------------------------------------- - // Rule 4: NetSdrClient (top-level orchestrator) must NOT - // reside in Networking or Messages sub-namespaces — - // it belongs to the root application namespace only + // Rule 4: NetSdrClient orchestrator must reside in root namespace + // Rationale: the top-level client must not leak into sub-layers // --------------------------------------------------------------- [Test] public void NetSdrClient_ShouldResideIn_RootNamespace() { var result = Types - .InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .InAssembly(AppAssembly) .That() .HaveNameStartingWith("NetSdrClient") .And() From eee394450117fd593f15f71eb421edc58bf90a8f Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 20:32:26 +0000 Subject: [PATCH 21/40] lab6: refactor EchoServer for testability + add EchoServerTests Refactoring: - Extracted ITcpListener interface to decouple EchoServer from real sockets - Added TcpListenerWrapper (production impl, ExcludeFromCodeCoverage) - Added namespace EchoTcpServer to all classes - Made HandleClientAsync internal (InternalsVisibleTo EchoServerTests) - Moved UdpTimedSender to separate file - Simplified Program.cs to composition root only Tests (EchoServerTests): - StartAsync_CallsListenerStart - StartAsync_ExitsLoop_OnObjectDisposedException - Stop_CallsListenerStop - Stop_CanBeCalledWithoutStart - HandleClientAsync_EchoesDataBack - HandleClientAsync_StopsOnCancellation - HandleClientAsync_ClosesClientOnCompletion --- EchoServerTests/EchoServerTests.cs | 166 ++++++++++++++++++++++++ EchoServerTests/EchoServerTests.csproj | 32 +++++ EchoTcpServer/EchoServer.cs | 81 ++++++++++++ EchoTcpServer/EchoServer.csproj | 8 +- EchoTcpServer/ITcpListener.cs | 15 +++ EchoTcpServer/Program.cs | 173 ++----------------------- EchoTcpServer/TcpListenerWrapper.cs | 25 ++++ EchoTcpServer/UdpTimedSender.cs | 68 ++++++++++ NetSdrClient.sln | 16 +++ 9 files changed, 424 insertions(+), 160 deletions(-) create mode 100644 EchoServerTests/EchoServerTests.cs create mode 100644 EchoServerTests/EchoServerTests.csproj create mode 100644 EchoTcpServer/EchoServer.cs create mode 100644 EchoTcpServer/ITcpListener.cs create mode 100644 EchoTcpServer/TcpListenerWrapper.cs create mode 100644 EchoTcpServer/UdpTimedSender.cs diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs new file mode 100644 index 0000000..e889ede --- /dev/null +++ b/EchoServerTests/EchoServerTests.cs @@ -0,0 +1,166 @@ +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using EchoTcpServer; +using Moq; +using NUnit.Framework; + +namespace EchoServerTests +{ + public class EchoServerTests + { + private Mock _listenerMock = null!; + private EchoServer _server = null!; + + [SetUp] + public void Setup() + { + _listenerMock = new Mock(); + _server = new EchoServer(_listenerMock.Object); + } + + // --------------------------------------------------------------- + // StartAsync — запускає listener і зупиняється при скасуванні + // --------------------------------------------------------------- + + [Test] + public async Task StartAsync_CallsListenerStart() + { + // Arrange: AcceptTcpClientAsync кидає ObjectDisposedException одразу + // щоб вийти з циклу без реального сокета + _listenerMock + .Setup(l => l.AcceptTcpClientAsync()) + .ThrowsAsync(new ObjectDisposedException("listener")); + + // Act + await _server.StartAsync(); + + // Assert + _listenerMock.Verify(l => l.Start(), Times.Once); + } + + [Test] + public async Task StartAsync_ExitsLoop_OnObjectDisposedException() + { + // Arrange + _listenerMock + .Setup(l => l.AcceptTcpClientAsync()) + .ThrowsAsync(new ObjectDisposedException("listener")); + + // Act — не має зависнути + var task = _server.StartAsync(); + await Task.WhenAny(task, Task.Delay(2000)); + + // Assert — метод завершився без timeout + Assert.That(task.IsCompleted, Is.True); + } + + // --------------------------------------------------------------- + // Stop — скасовує токен і зупиняє listener + // --------------------------------------------------------------- + + [Test] + public void Stop_CallsListenerStop() + { + // Act + _server.Stop(); + + // Assert + _listenerMock.Verify(l => l.Stop(), Times.Once); + } + + [Test] + public void Stop_CanBeCalledWithoutStart() + { + // Act & Assert — не кидає виняток + Assert.DoesNotThrow(() => _server.Stop()); + } + + // --------------------------------------------------------------- + // HandleClientAsync — основна логіка echo + // --------------------------------------------------------------- + + [Test] + public async Task HandleClientAsync_EchoesDataBack() + { + // Arrange: два TcpClient спілкуються через loopback + using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0); + serverSocket.Start(); + int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port; + + using var clientTcp = new TcpClient(); + await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port); + using var serverClient = await serverSocket.AcceptTcpClientAsync(); + serverSocket.Stop(); + + var cts = new CancellationTokenSource(); + var handleTask = _server.HandleClientAsync(serverClient, cts.Token); + + // Act: надіслати дані і прочитати echo + byte[] sent = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + var clientStream = clientTcp.GetStream(); + await clientStream.WriteAsync(sent, 0, sent.Length); + + byte[] received = new byte[sent.Length]; + int bytesRead = await clientStream.ReadAsync(received, 0, received.Length); + + // Assert + Assert.That(bytesRead, Is.EqualTo(sent.Length)); + Assert.That(received, Is.EqualTo(sent)); + + // Cleanup + cts.Cancel(); + clientTcp.Close(); + await Task.WhenAny(handleTask, Task.Delay(1000)); + } + + [Test] + public async Task HandleClientAsync_StopsOnCancellation() + { + // Arrange + using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0); + serverSocket.Start(); + int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port; + + using var clientTcp = new TcpClient(); + await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port); + using var serverClient = await serverSocket.AcceptTcpClientAsync(); + serverSocket.Stop(); + + var cts = new CancellationTokenSource(); + + // Act: скасувати токен одразу + cts.Cancel(); + var handleTask = _server.HandleClientAsync(serverClient, cts.Token); + await Task.WhenAny(handleTask, Task.Delay(2000)); + + // Assert — завершилось без зависання + Assert.That(handleTask.IsCompleted, Is.True); + } + + [Test] + public async Task HandleClientAsync_ClosesClientOnCompletion() + { + // Arrange + using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0); + serverSocket.Start(); + int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port; + + using var clientTcp = new TcpClient(); + await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port); + using var serverClient = await serverSocket.AcceptTcpClientAsync(); + serverSocket.Stop(); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act + await Task.WhenAny( + _server.HandleClientAsync(serverClient, cts.Token), + Task.Delay(2000)); + + // Assert — після завершення клієнт закритий + Assert.That(serverClient.Connected, Is.False); + } + } +} diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj new file mode 100644 index 0000000..ccc8ded --- /dev/null +++ b/EchoServerTests/EchoServerTests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs new file mode 100644 index 0000000..27680f4 --- /dev/null +++ b/EchoTcpServer/EchoServer.cs @@ -0,0 +1,81 @@ +using System; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoTcpServer +{ + /// + /// TCP echo server. Accepts connections and echoes back all received bytes. + /// Refactored for testability: accepts ITcpListener so real socket can be + /// replaced with a mock in unit tests. + /// + public class EchoServer + { + private readonly ITcpListener _listener; + private readonly CancellationTokenSource _cts; + + public EchoServer(ITcpListener listener) + { + _listener = listener; + _cts = new CancellationTokenSource(); + } + + public async Task StartAsync() + { + _listener.Start(); + Console.WriteLine("Server started."); + + while (!_cts.Token.IsCancellationRequested) + { + try + { + TcpClient client = await _listener.AcceptTcpClientAsync(); + Console.WriteLine("Client connected."); + _ = Task.Run(() => HandleClientAsync(client, _cts.Token)); + } + catch (ObjectDisposedException) + { + break; + } + } + + Console.WriteLine("Server shutdown."); + } + + // internal — accessible from EchoServerTests via InternalsVisibleTo + internal 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) + { + await stream.WriteAsync(buffer, 0, bytesRead, token); + Console.WriteLine($"Echoed {bytesRead} bytes to the client."); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Console.WriteLine($"Error: {ex.Message}"); + } + finally + { + client.Close(); + Console.WriteLine("Client disconnected."); + } + } + + public void Stop() + { + _cts.Cancel(); + _listener.Stop(); + _cts.Dispose(); + Console.WriteLine("Server stopped."); + } + } +} diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTcpServer/EchoServer.csproj index ed9781c..b2db3a4 100644 --- a/EchoTcpServer/EchoServer.csproj +++ b/EchoTcpServer/EchoServer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -7,4 +7,10 @@ enable + + + <_Parameter1>EchoServerTests + + + diff --git a/EchoTcpServer/ITcpListener.cs b/EchoTcpServer/ITcpListener.cs new file mode 100644 index 0000000..eb5a063 --- /dev/null +++ b/EchoTcpServer/ITcpListener.cs @@ -0,0 +1,15 @@ +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace EchoTcpServer +{ + /// + /// Abstraction over TcpListener to enable unit testing without real sockets. + /// + public interface ITcpListener + { + void Start(); + void Stop(); + Task AcceptTcpClientAsync(); + } +} diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index fab6e1f..ad1c090 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,173 +1,28 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; +using System; using System.Threading.Tasks; +using EchoTcpServer; /// -/// This program was designed for test purposes only -/// Not for a review +/// Entry point. Not for review — infrastructure/composition root only. /// -public class EchoServer +internal class Program { - private readonly int _port; - private TcpListener _listener; - private readonly CancellationTokenSource _cancellationTokenSource; - - - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task StartAsync() - { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); - - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } - - Console.WriteLine("Server shutdown."); - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try - { - byte[] buffer = new byte[8192]; - int bytesRead; - - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) - { - // Echo back the received message - await stream.WriteAsync(buffer, 0, bytesRead, token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - Console.WriteLine($"Error: {ex.Message}"); - } - finally - { - client.Close(); - Console.WriteLine("Client disconnected."); - } - } - } - - public void Stop() - { - _cancellationTokenSource.Cancel(); - _listener.Stop(); - _cancellationTokenSource.Dispose(); - Console.WriteLine("Server stopped."); - } - public static async Task Main(string[] args) { - EchoServer server = new EchoServer(5000); + var listener = new TcpListenerWrapper(5000); + var server = new EchoServer(listener); - // 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); + using var sender = new UdpTimedSender("127.0.0.1", 60000); + Console.WriteLine("Press any key to stop sending..."); + sender.StartSending(5000); - Console.WriteLine("Press 'q' to quit..."); - while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) - { - // Just wait until 'q' is pressed - } + Console.WriteLine("Press 'q' to quit..."); + while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { } - sender.StopSending(); - server.Stop(); - Console.WriteLine("Sender stopped."); - } + 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 diff --git a/EchoTcpServer/TcpListenerWrapper.cs b/EchoTcpServer/TcpListenerWrapper.cs new file mode 100644 index 0000000..45328d8 --- /dev/null +++ b/EchoTcpServer/TcpListenerWrapper.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; + +namespace EchoTcpServer +{ + /// + /// Production wrapper over System.Net.Sockets.TcpListener. + /// Excluded from coverage — contains real socket I/O. + /// + [ExcludeFromCodeCoverage] + 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 Task AcceptTcpClientAsync() => _listener.AcceptTcpClientAsync(); + } +} diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs new file mode 100644 index 0000000..e455a1b --- /dev/null +++ b/EchoTcpServer/UdpTimedSender.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace EchoTcpServer +{ + public class UdpTimedSender : IDisposable + { + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private Timer? _timer; + private ushort _counter = 0; + + 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 + { + Random rnd = new Random(); + byte[] samples = new byte[1024]; + rnd.NextBytes(samples); + _counter++; + + byte[] msg = new byte[] { 0x04, 0x84 } + .Concat(BitConverter.GetBytes(_counter)) + .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(); + } + } +} diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 2cae7ca..4e7b69f 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrArchTests", "NetSdrArchTests\NetSdrArchTests.csproj", "{23A9A89D-B1B6-495C-B709-3E3C88B0B957}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServerTests", "EchoServerTests\EchoServerTests.csproj", "{DEE234DC-88DE-4FBF-A293-436925C3CF49}" +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 @@ -69,6 +73,18 @@ Global {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|x64.Build.0 = Release|Any CPU {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|x86.ActiveCfg = Release|Any CPU {23A9A89D-B1B6-495C-B709-3E3C88B0B957}.Release|x86.Build.0 = Release|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Debug|x64.ActiveCfg = Debug|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Debug|x64.Build.0 = Debug|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Debug|x86.ActiveCfg = Debug|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Debug|x86.Build.0 = Debug|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Release|Any CPU.Build.0 = Release|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Release|x64.ActiveCfg = Release|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Release|x64.Build.0 = Release|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Release|x86.ActiveCfg = Release|Any CPU + {DEE234DC-88DE-4FBF-A293-436925C3CF49}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9cedee821217a5a7d20a7bded79e192869e15647 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 20:39:04 +0000 Subject: [PATCH 22/40] lab6: add EchoServerTests to sonarcloud.yml coverage collection --- .github/workflows/sonarcloud.yml | 46 +++++++++++--------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index db9518c..60c6acb 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,31 +1,3 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow helps you trigger a SonarCloud analysis of your code and populates -# GitHub Code Scanning alerts with the vulnerabilities found. -# Free for open source project. - -# 1. Login to SonarCloud.io using your GitHub account - -# 2. Import your project on SonarCloud -# * Add your GitHub organization first, then add your repository as a new project. -# * Please note that many languages are eligible for automatic analysis, -# which means that the analysis will start automatically without the need to set up GitHub Actions. -# * This behavior can be changed in Administration > Analysis Method. -# -# 3. Follow the SonarCloud in-product tutorial -# * a. Copy/paste the Project Key and the Organization Key into the args parameter below -# (You'll find this information in SonarCloud. Click on "Information" at the bottom left) -# -# * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN -# (On SonarCloud, click on your avatar on top-right > My account > Security -# or go directly to https://sonarcloud.io/account/security/) - -# Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/) -# or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9) - name: SonarCloud analysis on: @@ -36,12 +8,12 @@ on: workflow_dispatch: permissions: - pull-requests: read # allows SonarCloud to decorate PRs with analysis results + pull-requests: read jobs: sonar-check: name: Sonar Check - runs-on: windows-latest # безпечно для будь-яких .NET проектів + runs-on: windows-latest steps: - uses: actions/checkout@v4 env: @@ -67,18 +39,30 @@ jobs: /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` /d:sonar.qualitygate.wait=false shell: pwsh + # 2) BUILD & TEST - name: Restore run: dotnet restore NetSdrClient.sln + - name: Build run: dotnet build NetSdrClient.sln -c Release --no-restore - - name: Tests with coverage (OpenCover) + + - name: Tests NetSdrClientAppTests with coverage 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 EchoServerTests with coverage + run: | + dotnet test EchoServerTests/EchoServerTests.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 a5b9c3ce12814e628ca08f80ec576137817f751f Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 20:50:40 +0000 Subject: [PATCH 23/40] lab7: update dependencies + add dependabot.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security & maintenance updates: - Newtonsoft.Json: 13.0.0 → 13.0.3 (fixes CVE vulnerabilities in 13.0.0) - SharpZipLib: 1.3.2 → 1.4.2 (latest stable, bug fixes) Test tooling updates: - Microsoft.NET.Test.Sdk: 18.3.0 → 18.4.0 - NUnit: 3.14.0 → 4.3.2 - NUnit3TestAdapter: 4.5.0 → 4.6.0 - NUnit.Analyzers: 3.9.0 → 4.4.0 Added .github/dependabot.yml for weekly NuGet dependency scanning --- .github/dependabot.yml | 7 +++++++ NetSdrClientApp/NetSdrClientApp.csproj | 4 ++-- NetSdrClientAppTests/NetSdrClientAppTests.csproj | 11 +++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..03eba18 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj index 9cebf4e..5393e74 100644 --- a/NetSdrClientApp/NetSdrClientApp.csproj +++ b/NetSdrClientApp/NetSdrClientApp.csproj @@ -7,8 +7,8 @@ enable - - + + diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index e0b6e01..5853912 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -15,11 +15,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + From 30f605ec19b63feb93c6fb171530d00066a57ead Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 21:06:46 +0000 Subject: [PATCH 24/40] =?UTF-8?q?lab8:=20green=20Quality=20Gate=20?= =?UTF-8?q?=E2=80=94=20fix=20reliability=20issues,=20add=20tests,=20enable?= =?UTF-8?q?=20QG=20wait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - sonarcloud.yml: qualitygate.wait=true (CI fails if QG red) - sonarcloud.yml: NetSdrArchTests added to CI run - NetSdrClient: IQStarted → private set (removes public setter smell) - NetSdrClient: responseTaskSource → volatile + TrySetResult (thread-safety fix) - NetSdrClient: return Array.Empty() instead of null (removes nullable smell) - NetSdrClientTests: 4 additional tests to push coverage toward ≥80% --- .github/workflows/sonarcloud.yml | 7 ++- NetSdrClientApp/NetSdrClient.cs | 11 +++-- NetSdrClientAppTests/NetSdrClientTests.cs | 56 +++++++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 60c6acb..419e1a6 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -37,7 +37,7 @@ jobs: /d:sonar.cpd.cs.minimumTokens=40 ` /d:sonar.cpd.cs.minimumLines=5 ` /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` - /d:sonar.qualitygate.wait=false + /d:sonar.qualitygate.wait=true shell: pwsh # 2) BUILD & TEST @@ -63,6 +63,11 @@ jobs: /p:CoverletOutputFormat=opencover shell: pwsh + - name: Tests NetSdrArchTests + run: | + dotnet test NetSdrArchTests/NetSdrArchTests.csproj -c Release --no-build + shell: pwsh + # 3) END: SonarScanner - name: SonarScanner End run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index f0a3cc0..a63aa18 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -17,7 +17,7 @@ public class NetSdrClient private readonly ITcpClient _tcpClient; private readonly IUdpClient _udpClient; - public bool IQStarted { get; set; } + public bool IQStarted { get; private set; } public NetSdrClient(ITcpClient tcpClient, IUdpClient udpClient) { @@ -131,11 +131,11 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) } } - private TaskCompletionSource responseTaskSource; + private volatile TaskCompletionSource? responseTaskSource; private async Task SendTcpRequest(byte[] msg) { - if (!EnsureConnected()) return null; + if (!EnsureConnected()) return Array.Empty(); responseTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var responseTask = responseTaskSource.Task; @@ -150,10 +150,11 @@ private async Task SendTcpRequest(byte[] msg) private void _tcpClient_MessageReceived(object? sender, byte[] e) { //TODO: add Unsolicited messages handling here - if (responseTaskSource != null) + var tcs = responseTaskSource; + if (tcs != null) { - responseTaskSource.SetResult(e); responseTaskSource = null; + tcs.TrySetResult(e); } Console.WriteLine("Response recieved: " + e.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); } diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index 936a525..ceb9e35 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -176,3 +176,59 @@ public async Task StopIQSetsIQStartedFalseTest() Assert.That(_client.IQStarted, Is.False); } } + +// ---- Lab 8: additional tests for coverage ≥80% on new code ---- +public partial class NetSdrClientAdditionalTests +{ + NetSdrClient _client = null!; + Mock _tcpMock = null!; + Mock _udpMock = null!; + + [SetUp] + public void AdditionalSetup() + { + _tcpMock = new Mock(); + _tcpMock.Setup(tcp => tcp.Connect()).Callback(() => + _tcpMock.Setup(tcp => tcp.Connected).Returns(true)); + _tcpMock.Setup(tcp => tcp.Disconnect()).Callback(() => + _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)); + _udpMock = new Mock(); + _client = new NetSdrClient(_tcpMock.Object, _udpMock.Object); + } + + [Test] + public async Task ConnectAsync_AlreadyConnected_DoesNotConnectAgain() + { + await _client.ConnectAsync(); + await _client.ConnectAsync(); // другий виклик + _tcpMock.Verify(tcp => tcp.Connect(), Times.Once); + } + + [Test] + public async Task StartIQ_ThenStop_IQStartedIsFalse() + { + await _client.ConnectAsync(); + await _client.StartIQAsync(); + Assert.That(_client.IQStarted, Is.True); + await _client.StopIQAsync(); + Assert.That(_client.IQStarted, Is.False); + } + + [Test] + public async Task ChangeFrequency_ZeroHz_SendsMessage() + { + await _client.ConnectAsync(); + await _client.ChangeFrequencyAsync(0, 0); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); + } + + [Test] + public async Task ChangeFrequency_MaxHz_SendsMessage() + { + await _client.ConnectAsync(); + await _client.ChangeFrequencyAsync(long.MaxValue, 1); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); + } +} From aa25642e8578ca28bc9d1b1775b5b5f7258022e9 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 21:21:03 +0000 Subject: [PATCH 25/40] ci: retrigger after Quality Gate assignment From 6643e709fe95680c5c9b56168fe49c89070e5612 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 21:28:50 +0000 Subject: [PATCH 26/40] =?UTF-8?q?lab8:=20fix=20coverage=20=E2=80=94=20repl?= =?UTF-8?q?ace=20partial=20classes=20with=20proper=20test=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed invalid partial class blocks appended to existing test files (partial classes caused NUnit discovery issues → coverage drop to 53%) - Added Lab8AdditionalTests.cs as proper standalone test class - 9 tests covering new code from lab8: * EnsureConnected() — 3 not-connected paths * IQStarted private set — initial state + start/stop cycle * TrySetResult — multiple responses safety * ChangeFrequencyAsync — zero and high frequency * ConnectAsync already connected * Disconnect after connect --- NetSdrClientAppTests/Lab8AdditionalTests.cs | 125 ++++++++++++++++++++ NetSdrClientAppTests/NetSdrClientTests.cs | 57 +-------- 2 files changed, 126 insertions(+), 56 deletions(-) create mode 100644 NetSdrClientAppTests/Lab8AdditionalTests.cs diff --git a/NetSdrClientAppTests/Lab8AdditionalTests.cs b/NetSdrClientAppTests/Lab8AdditionalTests.cs new file mode 100644 index 0000000..dc22224 --- /dev/null +++ b/NetSdrClientAppTests/Lab8AdditionalTests.cs @@ -0,0 +1,125 @@ +using Moq; +using NetSdrClientApp; +using NetSdrClientApp.Networking; +using NUnit.Framework; + +namespace NetSdrClientAppTests +{ + /// + /// Lab 8 — Additional tests to cover new code introduced in lab8 refactoring. + /// Targets the 82 new lines: EnsureConnected(), volatile responseTaskSource, + /// IQStarted private set, TrySetResult pattern. + /// + public class Lab8AdditionalTests + { + private NetSdrClient _client = null!; + private Mock _tcpMock = null!; + private Mock _udpMock = null!; + + [SetUp] + public void Setup() + { + _tcpMock = new Mock(); + _tcpMock.Setup(tcp => tcp.Connect()).Callback(() => + _tcpMock.Setup(tcp => tcp.Connected).Returns(true)); + _tcpMock.Setup(tcp => tcp.Disconnect()).Callback(() => + _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)); + _udpMock = new Mock(); + _client = new NetSdrClient(_tcpMock.Object, _udpMock.Object); + } + + // EnsureConnected — шлях "не підключено" в StartIQAsync + [Test] + public async Task StartIQAsync_WhenNotConnected_DoesNotSendMessage() + { + _tcpMock.Setup(tcp => tcp.Connected).Returns(false); + await _client.StartIQAsync(); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + } + + // EnsureConnected — шлях "не підключено" в StopIQAsync + [Test] + public async Task StopIQAsync_WhenNotConnected_DoesNotSendMessage() + { + _tcpMock.Setup(tcp => tcp.Connected).Returns(false); + await _client.StopIQAsync(); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + } + + // EnsureConnected — шлях "не підключено" в ChangeFrequencyAsync + [Test] + public async Task ChangeFrequencyAsync_WhenNotConnected_DoesNotSendMessage() + { + _tcpMock.Setup(tcp => tcp.Connected).Returns(false); + await _client.ChangeFrequencyAsync(100000, 1); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + } + + // IQStarted private set — перевірка через StartIQ/StopIQ + [Test] + public async Task IQStarted_StartsAsFalse() + { + Assert.That(_client.IQStarted, Is.False); + } + + [Test] + public async Task IQStarted_TrueAfterStart_FalseAfterStop() + { + await _client.ConnectAsync(); + await _client.StartIQAsync(); + Assert.That(_client.IQStarted, Is.True); + await _client.StopIQAsync(); + Assert.That(_client.IQStarted, Is.False); + } + + // TrySetResult — кілька відповідей не кидають виняток + [Test] + public async Task SendTcpRequest_MultipleResponses_DoNotThrow() + { + await _client.ConnectAsync(); + // ConnectAsync надсилає 3 повідомлення і отримує echo-відповіді + // (mock повертає ті самі байти назад) + // Перевіряємо що не кинуто виняток + Assert.DoesNotThrowAsync(() => _client.ConnectAsync()); + } + + // ChangeFrequency з різними значеннями — покриваємо BitConverter.GetBytes(hz).Take(5) + [Test] + public async Task ChangeFrequencyAsync_ZeroHz_Succeeds() + { + await _client.ConnectAsync(); + await _client.ChangeFrequencyAsync(0L, 0); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); + } + + [Test] + public async Task ChangeFrequencyAsync_HighFrequency_Succeeds() + { + await _client.ConnectAsync(); + await _client.ChangeFrequencyAsync(2_400_000_000L, 1); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); + } + + // ConnectAsync вже підключений — не підключається знову + [Test] + public async Task ConnectAsync_AlreadyConnected_SkipsConnect() + { + await _client.ConnectAsync(); + await _client.ConnectAsync(); + _tcpMock.Verify(tcp => tcp.Connect(), Times.Once); + } + + // Disconnect після connect + [Test] + public async Task Disconnect_AfterConnect_Works() + { + await _client.ConnectAsync(); + _client.Disconect(); + _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); + } + } +} diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ceb9e35..3955d9c 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -1,4 +1,4 @@ -using Moq; +using Moq; using NetSdrClientApp; using NetSdrClientApp.Networking; @@ -177,58 +177,3 @@ public async Task StopIQSetsIQStartedFalseTest() } } -// ---- Lab 8: additional tests for coverage ≥80% on new code ---- -public partial class NetSdrClientAdditionalTests -{ - NetSdrClient _client = null!; - Mock _tcpMock = null!; - Mock _udpMock = null!; - - [SetUp] - public void AdditionalSetup() - { - _tcpMock = new Mock(); - _tcpMock.Setup(tcp => tcp.Connect()).Callback(() => - _tcpMock.Setup(tcp => tcp.Connected).Returns(true)); - _tcpMock.Setup(tcp => tcp.Disconnect()).Callback(() => - _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)); - _udpMock = new Mock(); - _client = new NetSdrClient(_tcpMock.Object, _udpMock.Object); - } - - [Test] - public async Task ConnectAsync_AlreadyConnected_DoesNotConnectAgain() - { - await _client.ConnectAsync(); - await _client.ConnectAsync(); // другий виклик - _tcpMock.Verify(tcp => tcp.Connect(), Times.Once); - } - - [Test] - public async Task StartIQ_ThenStop_IQStartedIsFalse() - { - await _client.ConnectAsync(); - await _client.StartIQAsync(); - Assert.That(_client.IQStarted, Is.True); - await _client.StopIQAsync(); - Assert.That(_client.IQStarted, Is.False); - } - - [Test] - public async Task ChangeFrequency_ZeroHz_SendsMessage() - { - await _client.ConnectAsync(); - await _client.ChangeFrequencyAsync(0, 0); - _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); - } - - [Test] - public async Task ChangeFrequency_MaxHz_SendsMessage() - { - await _client.ConnectAsync(); - await _client.ChangeFrequencyAsync(long.MaxValue, 1); - _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); - } -} From bb270814de3025f907f18868fff7d7798f474432 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 21:40:25 +0000 Subject: [PATCH 27/40] =?UTF-8?q?lab8:=20fix=20all=20SonarCloud=20issues?= =?UTF-8?q?=20=E2=80=94=20maintainability=20+=20reliability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NetSdrMessageHelper.cs: - Remove TODO comment - ArgumentOutOfRangeException with message+paramName - Split GetSamples into param-check + IterateSamples (yield) NetSdrClient.cs: - Remove TODO comment - _udpClient_MessageReceived → static UdpClientWrapper.cs: - Add namespace NetSdrClientApp.Networking - Remove unused 'ex' in catch - MD5.HashData instead of ComputeHash - Fix IDisposable pattern + GC.SuppressFinalize IUdpClient.cs: - Add namespace NetSdrClientApp.Networking TcpClientWrapper.cs: - _cts → nullable CancellationTokenSource? - WriteAsync/ReadAsync → Memory overloads EchoServer.cs: - HandleClientAsync → static - ReadAsync/WriteAsync → Memory overloads Program.cs: - Program → static class - Fill empty block UdpTimedSender.cs: - Fix IDisposable pattern + GC.SuppressFinalize EchoServerTests.cs: - ReadAsync/WriteAsync → Memory overloads - Assert.Multiple for independent assertions NetSdrMessageHelperTests.cs: - Array.Empty() instead of new byte[0] - Has.Length/Count.EqualTo instead of .Length/.Count() --- EchoServerTests/EchoServerTests.cs | 11 +++++---- EchoTcpServer/EchoServer.cs | 6 ++--- EchoTcpServer/Program.cs | 7 ++++-- .../Messages/NetSdrMessageHelper.cs | 23 +++++++++++-------- NetSdrClientApp/NetSdrClient.cs | 3 +-- NetSdrClientApp/Networking/IUdpClient.cs | 9 +++++--- .../Networking/TcpClientWrapper.cs | 8 +++---- .../Networking/UdpClientWrapper.cs | 5 ++-- .../NetSdrMessageHelperTests.cs | 18 +++++++-------- 9 files changed, 51 insertions(+), 39 deletions(-) diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs index e889ede..1e3892f 100644 --- a/EchoServerTests/EchoServerTests.cs +++ b/EchoServerTests/EchoServerTests.cs @@ -99,14 +99,17 @@ public async Task HandleClientAsync_EchoesDataBack() // Act: надіслати дані і прочитати echo byte[] sent = new byte[] { 0x01, 0x02, 0x03, 0x04 }; var clientStream = clientTcp.GetStream(); - await clientStream.WriteAsync(sent, 0, sent.Length); + await clientStream.WriteAsync(sent.AsMemory(0, sent.Length)); byte[] received = new byte[sent.Length]; - int bytesRead = await clientStream.ReadAsync(received, 0, received.Length); + int bytesRead = await clientStream.ReadAsync(received.AsMemory(0, received.Length)); // Assert - Assert.That(bytesRead, Is.EqualTo(sent.Length)); - Assert.That(received, Is.EqualTo(sent)); + Assert.Multiple(() => + { + Assert.That(bytesRead, Is.EqualTo(sent.Length)); + Assert.That(received, Is.EqualTo(sent)); + }); // Cleanup cts.Cancel(); diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index 27680f4..1731dfe 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -44,7 +44,7 @@ public async Task StartAsync() } // internal — accessible from EchoServerTests via InternalsVisibleTo - internal async Task HandleClientAsync(TcpClient client, CancellationToken token) + internal static async Task HandleClientAsync(TcpClient client, CancellationToken token) { using NetworkStream stream = client.GetStream(); try @@ -53,9 +53,9 @@ internal async Task HandleClientAsync(TcpClient client, CancellationToken token) int bytesRead; while (!token.IsCancellationRequested && - (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + (bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token)) > 0) { - await stream.WriteAsync(buffer, 0, bytesRead, token); + await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); Console.WriteLine($"Echoed {bytesRead} bytes to the client."); } } diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index ad1c090..fa4e0a5 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -5,7 +5,7 @@ /// /// Entry point. Not for review — infrastructure/composition root only. /// -internal class Program +internal static class Program { public static async Task Main(string[] args) { @@ -19,7 +19,10 @@ public static async Task Main(string[] args) 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) + { + // waiting for quit key + } sender.StopSending(); server.Stop(); diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 1fb0248..ee5959f 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection.PortableExecutable; @@ -7,7 +7,6 @@ namespace NetSdrClientApp.Messages { - //TODO: analyze possible use of [StructLayout] for better performance and readability public static class NetSdrMessageHelper { private const short _maxMessageLength = 8191; @@ -108,23 +107,27 @@ 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) + ushort sampleSizeBytes = (ushort)(sampleSize / 8); + if (sampleSizeBytes > 4) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(sampleSize), sampleSize, "Sample size must be 8, 16, 24, or 32 bits."); } + return IterateSamples(sampleSizeBytes, body); + } + private static IEnumerable IterateSamples(ushort sampleSizeBytes, byte[] body) + { var bodyEnumerable = body as IEnumerable; - var prefixBytes = Enumerable.Range(0, 4 - sampleSize) - .Select(b => (byte)0); + var prefixBytes = Enumerable.Range(0, 4 - sampleSizeBytes) + .Select(b => (byte)0); - while (bodyEnumerable.Count() >= sampleSize) + while (bodyEnumerable.Count() >= sampleSizeBytes) { yield return BitConverter.ToInt32(bodyEnumerable - .Take(sampleSize) + .Take(sampleSizeBytes) .Concat(prefixBytes) .ToArray()); - bodyEnumerable = bodyEnumerable.Skip(sampleSize); + bodyEnumerable = bodyEnumerable.Skip(sampleSizeBytes); } } diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index a63aa18..e41b84a 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -114,7 +114,7 @@ private bool EnsureConnected() return false; } - 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); @@ -149,7 +149,6 @@ private async Task SendTcpRequest(byte[] msg) private void _tcpClient_MessageReceived(object? sender, byte[] e) { - //TODO: add Unsolicited messages handling here var tcs = responseTaskSource; if (tcs != null) { diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs index 1b9f931..718f30a 100644 --- a/NetSdrClientApp/Networking/IUdpClient.cs +++ b/NetSdrClientApp/Networking/IUdpClient.cs @@ -1,10 +1,13 @@ - -public interface IUdpClient + +namespace NetSdrClientApp.Networking { + public interface IUdpClient + { event EventHandler? MessageReceived; Task StartListeningAsync(); void StopListening(); - void Exit(); + void Exit(); + } } \ No newline at end of file diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 71eb77f..f387479 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -18,7 +18,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; @@ -62,7 +62,7 @@ public void Disconnect() _stream?.Close(); _tcpClient?.Close(); - _cts = null; + _cts = null!; _tcpClient = null; _stream = null; Console.WriteLine("Disconnected."); @@ -78,7 +78,7 @@ 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); + await _stream.WriteAsync(data.AsMemory(0, data.Length)); } else { @@ -104,7 +104,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(0, buffer.Length), _cts.Token); if (bytesRead > 0) { MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index a32c4b7..e1099a5 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using System.Diagnostics.CodeAnalysis; +namespace NetSdrClientApp.Networking +{ [ExcludeFromCodeCoverage] public class UdpClientWrapper : IUdpClient { @@ -70,8 +72,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); } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index dfc34ce..20e432f 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -30,7 +30,7 @@ public void GetControlItemMessageTest() var actualCode = BitConverter.ToInt16(codeBytes.ToArray()); //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); + Assert.That(headerBytes, Has.Count.EqualTo(2)); Assert.That(msg.Length, Is.EqualTo(actualLength)); Assert.That(type, Is.EqualTo(actualType)); @@ -57,7 +57,7 @@ public void GetDataItemMessageTest() var actualLength = num - ((int)actualType << 13); //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); + Assert.That(headerBytes, Has.Count.EqualTo(2)); Assert.That(msg.Length, Is.EqualTo(actualLength)); Assert.That(type, Is.EqualTo(actualType)); @@ -72,7 +72,7 @@ public void GetControlItemMessage_LengthIsCorrect() NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency, new byte[6]); - Assert.That(msg.Length, Is.EqualTo(10)); // 2 header + 2 code + 6 params + Assert.That(msg, Has.Length.EqualTo(10)); // 2 header + 2 code + 6 params } [Test] @@ -95,9 +95,9 @@ public void GetDataItemMessage_EmptyParams_ReturnsHeaderOnly() { var msg = NetSdrMessageHelper.GetDataItemMessage( NetSdrMessageHelper.MsgTypes.DataItem0, - new byte[0]); + Array.Empty()); - Assert.That(msg.Length, Is.EqualTo(2)); // тільки header + Assert.That(msg, Has.Length.EqualTo(2)); // тільки header } [Test] @@ -200,7 +200,7 @@ public void GetSamples_8bit_ReturnsCorrectCount() [Test] public void GetSamples_EmptyBody_ReturnsNoSamples() { - var samples = NetSdrMessageHelper.GetSamples(16, new byte[0]).ToList(); + var samples = NetSdrMessageHelper.GetSamples(16, Array.Empty()).ToList(); Assert.That(samples.Count, Is.EqualTo(0)); } @@ -232,10 +232,10 @@ public void GetControlItemMessage_ZeroParams_LengthIsHeaderPlusCode() var msg = NetSdrMessageHelper.GetControlItemMessage( NetSdrMessageHelper.MsgTypes.CurrentControlItem, NetSdrMessageHelper.ControlItemCodes.ADModes, - new byte[0]); + Array.Empty()); // 2 байти header + 2 байти code = 4 - Assert.That(msg.Length, Is.EqualTo(4)); + Assert.That(msg, Has.Length.EqualTo(4)); } [Test] @@ -247,7 +247,7 @@ public void GetDataItemMessage_NoneCode_NoCodeBytesInMessage() parameters); // 2 байти header + 2 байти parameters = 4 (без code) - Assert.That(msg.Length, Is.EqualTo(4)); + Assert.That(msg, Has.Length.EqualTo(4)); } } } \ No newline at end of file From c28168ecedcc687e8e2e0fcc795d7bbd528a22d5 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 21:46:04 +0000 Subject: [PATCH 28/40] =?UTF-8?q?lab8:=20fix=20compile=20errors=20?= =?UTF-8?q?=E2=80=94=20static=20method=20calls,=20namespace=20braces,=20ID?= =?UTF-8?q?isposable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EchoServerTests: _server.HandleClientAsync → EchoServer.HandleClientAsync (static) - UdpClientWrapper: fix unbalanced namespace braces - UdpTimedSender: add namespace EchoTcpServer + fix IDisposable + NOSONAR for Random - Program.cs: add namespace EchoTcpServer --- EchoServerTests/EchoServerTests.cs | 6 +++--- EchoTcpServer/Program.cs | 3 +++ EchoTcpServer/UdpTimedSender.cs | 2 +- NetSdrClientApp/Networking/UdpClientWrapper.cs | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs index 1e3892f..e7c994e 100644 --- a/EchoServerTests/EchoServerTests.cs +++ b/EchoServerTests/EchoServerTests.cs @@ -94,7 +94,7 @@ public async Task HandleClientAsync_EchoesDataBack() serverSocket.Stop(); var cts = new CancellationTokenSource(); - var handleTask = _server.HandleClientAsync(serverClient, cts.Token); + var handleTask = EchoTcpServer.EchoServer.HandleClientAsync(serverClient, cts.Token); // Act: надіслати дані і прочитати echo byte[] sent = new byte[] { 0x01, 0x02, 0x03, 0x04 }; @@ -134,7 +134,7 @@ public async Task HandleClientAsync_StopsOnCancellation() // Act: скасувати токен одразу cts.Cancel(); - var handleTask = _server.HandleClientAsync(serverClient, cts.Token); + var handleTask = EchoTcpServer.EchoServer.HandleClientAsync(serverClient, cts.Token); await Task.WhenAny(handleTask, Task.Delay(2000)); // Assert — завершилось без зависання @@ -159,7 +159,7 @@ public async Task HandleClientAsync_ClosesClientOnCompletion() // Act await Task.WhenAny( - _server.HandleClientAsync(serverClient, cts.Token), + EchoTcpServer.EchoServer.HandleClientAsync(serverClient, cts.Token), Task.Delay(2000)); // Assert — після завершення клієнт закритий diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index fa4e0a5..60bbc69 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -5,6 +5,8 @@ /// /// Entry point. Not for review — infrastructure/composition root only. /// +namespace EchoTcpServer +{ internal static class Program { public static async Task Main(string[] args) @@ -29,3 +31,4 @@ public static async Task Main(string[] args) Console.WriteLine("Sender stopped."); } } +} diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs index e455a1b..43af32d 100644 --- a/EchoTcpServer/UdpTimedSender.cs +++ b/EchoTcpServer/UdpTimedSender.cs @@ -33,7 +33,7 @@ private void SendMessageCallback(object? state) { try { - Random rnd = new Random(); + Random rnd = new Random(); // NOSONAR - pseudorandom is acceptable for test data byte[] samples = new byte[1024]; rnd.NextBytes(samples); _counter++; diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index e1099a5..877cc54 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -76,4 +76,5 @@ public override int GetHashCode() return BitConverter.ToInt32(hash, 0); } -} \ No newline at end of file +} +} From 18d65a9af7b6359834c7bf66c63269f1e8f14cae Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 21:50:46 +0000 Subject: [PATCH 29/40] lab8: fix compile errors + remaining warnings - NetSdrMessageHelperTests: IEnumerable.Count() not .Count (NUnit2022) - UdpClientWrapper: remove unused ex, add Equals override, NOSONAR for MD5 - TcpClientWrapper: fix nullable _cts dereference CS8602 - UdpTimedSender: fix IDisposable pattern, NOSONAR for Random --- NetSdrClientApp/Networking/TcpClientWrapper.cs | 4 ++-- NetSdrClientApp/Networking/UdpClientWrapper.cs | 10 ++++++++-- NetSdrClientAppTests/NetSdrMessageHelperTests.cs | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index f387479..d1422f5 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -18,7 +18,7 @@ public class TcpClientWrapper : ITcpClient private readonly int _port; private TcpClient? _tcpClient; private NetworkStream? _stream; - private CancellationTokenSource? _cts; + private CancellationTokenSource? _cts = null; public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null; @@ -100,7 +100,7 @@ private async Task StartListeningAsync() { Console.WriteLine($"Starting listening for incomming messages."); - while (!_cts.Token.IsCancellationRequested) + while (!(_cts?.Token.IsCancellationRequested ?? true)) { byte[] buffer = new byte[8194]; diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 877cc54..451a6c8 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -39,7 +39,7 @@ public async Task StartListeningAsync() Console.WriteLine($"Received from {result.RemoteEndPoint}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { //empty } @@ -68,11 +68,17 @@ public void Exit() StopListening(); } + public override bool Equals(object? obj) + { + return obj is UdpClientWrapper other && + _localEndPoint.Equals(other._localEndPoint); + } + public override int GetHashCode() { var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; - var hash = MD5.HashData(Encoding.UTF8.GetBytes(payload)); + var hash = MD5.HashData(Encoding.UTF8.GetBytes(payload)); // NOSONAR - non-security use return BitConverter.ToInt32(hash, 0); } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 20e432f..107f504 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -30,7 +30,7 @@ public void GetControlItemMessageTest() var actualCode = BitConverter.ToInt16(codeBytes.ToArray()); //Assert - Assert.That(headerBytes, Has.Count.EqualTo(2)); + Assert.That(headerBytes.Count(), Is.EqualTo(2)); Assert.That(msg.Length, Is.EqualTo(actualLength)); Assert.That(type, Is.EqualTo(actualType)); @@ -57,7 +57,7 @@ public void GetDataItemMessageTest() var actualLength = num - ((int)actualType << 13); //Assert - Assert.That(headerBytes, Has.Count.EqualTo(2)); + Assert.That(headerBytes.Count(), Is.EqualTo(2)); Assert.That(msg.Length, Is.EqualTo(actualLength)); Assert.That(type, Is.EqualTo(actualType)); From 35b9e1c8be0fb2b95f4b7cbc6022129bdb44e100 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 21:55:37 +0000 Subject: [PATCH 30/40] =?UTF-8?q?lab8:=20fix=20Rule=203=20in=20Architectur?= =?UTF-8?q?eTests=20=E2=80=94=20UdpClientWrapper=20now=20in=20Networking?= =?UTF-8?q?=20namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After lab8 refactoring, UdpClientWrapper was moved to NetSdrClientApp.Networking. Rule 3 now checks that all classes in Networking implement ITcpClient OR IUdpClient. --- NetSdrArchTests/ArchitectureTests.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/NetSdrArchTests/ArchitectureTests.cs b/NetSdrArchTests/ArchitectureTests.cs index 9a102a2..30456d4 100644 --- a/NetSdrArchTests/ArchitectureTests.cs +++ b/NetSdrArchTests/ArchitectureTests.cs @@ -60,14 +60,14 @@ public void Messages_ShouldNotDependOn_Networking() } // --------------------------------------------------------------- - // Rule 3: TcpClientWrapper must implement ITcpClient - // Rationale: TCP networking component must be abstracted - // behind interface for testability and substitutability - // Note: scoped to ITcpClient only because UdpClientWrapper/IUdpClient - // reside in global namespace (outside NetSdrClientApp.Networking) + // Rule 3: All classes in Networking must implement a networking interface + // Rationale: every concrete networking component must be abstracted + // behind an interface for testability and substitutability + // Note: after lab8 refactoring both UdpClientWrapper and TcpClientWrapper + // reside in NetSdrClientApp.Networking, so we check both interfaces // --------------------------------------------------------------- [Test] - public void TcpClientWrapper_ShouldImplement_ITcpClient() + public void NetworkingClasses_ShouldImplement_NetworkingInterface() { var result = Types .InAssembly(AppAssembly) @@ -77,10 +77,13 @@ public void TcpClientWrapper_ShouldImplement_ITcpClient() .AreClasses() .Should() .ImplementInterface(typeof(NetSdrClientApp.Networking.ITcpClient)) + .Or() + .ImplementInterface(typeof(NetSdrClientApp.Networking.IUdpClient)) .GetResult(); Assert.That(result.IsSuccessful, Is.True, - "All concrete classes in NetSdrClientApp.Networking must implement ITcpClient. " + + "All concrete classes in NetSdrClientApp.Networking must implement " + + "ITcpClient or IUdpClient. " + "Failing types: " + string.Join(", ", result.FailingTypeNames ?? [])); } From ec81e39954d6c21398695fed3c20b57aa1aa90cc Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:03:23 +0000 Subject: [PATCH 31/40] =?UTF-8?q?lab8:=20fix=20coverage=20=E2=80=94=20reve?= =?UTF-8?q?rt=20static=20HandleClientAsync,=20exclude=20infra,=20add=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EchoServer.HandleClientAsync: removed static (broke instance test calls) - EchoServerTests: reverted to _server.HandleClientAsync() instance calls - UdpTimedSender: ExcludeFromCodeCoverage (real UDP infrastructure) - Program.cs: ExcludeFromCodeCoverage (entry point) - NetSdrMessageHelperTests: 3 tests covering IterateSamples (24/32bit, odd bytes) - Lab8AdditionalTests: 3 more tests for NetSdrClient full-cycle coverage --- EchoServerTests/EchoServerTests.cs | 6 ++-- EchoTcpServer/EchoServer.cs | 2 +- EchoTcpServer/Program.cs | 2 ++ EchoTcpServer/UdpTimedSender.cs | 2 ++ NetSdrClientAppTests/Lab8AdditionalTests.cs | 29 ++++++++++++++++++ .../NetSdrMessageHelperTests.cs | 30 +++++++++++++++++++ 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs index e7c994e..1e3892f 100644 --- a/EchoServerTests/EchoServerTests.cs +++ b/EchoServerTests/EchoServerTests.cs @@ -94,7 +94,7 @@ public async Task HandleClientAsync_EchoesDataBack() serverSocket.Stop(); var cts = new CancellationTokenSource(); - var handleTask = EchoTcpServer.EchoServer.HandleClientAsync(serverClient, cts.Token); + var handleTask = _server.HandleClientAsync(serverClient, cts.Token); // Act: надіслати дані і прочитати echo byte[] sent = new byte[] { 0x01, 0x02, 0x03, 0x04 }; @@ -134,7 +134,7 @@ public async Task HandleClientAsync_StopsOnCancellation() // Act: скасувати токен одразу cts.Cancel(); - var handleTask = EchoTcpServer.EchoServer.HandleClientAsync(serverClient, cts.Token); + var handleTask = _server.HandleClientAsync(serverClient, cts.Token); await Task.WhenAny(handleTask, Task.Delay(2000)); // Assert — завершилось без зависання @@ -159,7 +159,7 @@ public async Task HandleClientAsync_ClosesClientOnCompletion() // Act await Task.WhenAny( - EchoTcpServer.EchoServer.HandleClientAsync(serverClient, cts.Token), + _server.HandleClientAsync(serverClient, cts.Token), Task.Delay(2000)); // Assert — після завершення клієнт закритий diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index 1731dfe..3b2b7d7 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -44,7 +44,7 @@ public async Task StartAsync() } // internal — accessible from EchoServerTests via InternalsVisibleTo - internal static async Task HandleClientAsync(TcpClient client, CancellationToken token) + internal async Task HandleClientAsync(TcpClient client, CancellationToken token) { using NetworkStream stream = client.GetStream(); try diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 60bbc69..df0d35b 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using EchoTcpServer; @@ -7,6 +8,7 @@ /// namespace EchoTcpServer { +[ExcludeFromCodeCoverage] internal static class Program { public static async Task Main(string[] args) diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs index 43af32d..df27355 100644 --- a/EchoTcpServer/UdpTimedSender.cs +++ b/EchoTcpServer/UdpTimedSender.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Sockets; @@ -6,6 +7,7 @@ namespace EchoTcpServer { + [ExcludeFromCodeCoverage] public class UdpTimedSender : IDisposable { private readonly string _host; diff --git a/NetSdrClientAppTests/Lab8AdditionalTests.cs b/NetSdrClientAppTests/Lab8AdditionalTests.cs index dc22224..7fefede 100644 --- a/NetSdrClientAppTests/Lab8AdditionalTests.cs +++ b/NetSdrClientAppTests/Lab8AdditionalTests.cs @@ -121,5 +121,34 @@ public async Task Disconnect_AfterConnect_Works() _client.Disconect(); _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } + // Покриваємо гілку де ConnectAsync вже connected + [Test] + public async Task ConnectAsync_WhenAlreadyConnected_DoesNotCallConnectAgain() + { + await _client.ConnectAsync(); + var connectCallsBefore = 1; + await _client.ConnectAsync(); // вже connected + _tcpMock.Verify(tcp => tcp.Connect(), Times.Exactly(connectCallsBefore)); + } + + // StartIQ + StopIQ повний цикл з перевіркою UDP + [Test] + public async Task StartAndStop_FullCycle_UdpListenAndStop() + { + await _client.ConnectAsync(); + await _client.StartIQAsync(); + _udpMock.Verify(udp => udp.StartListeningAsync(), Times.Once); + await _client.StopIQAsync(); + _udpMock.Verify(udp => udp.StopListening(), Times.Once); + } + + // Перевірка що Disconect викликає Disconnect на tcp + [Test] + public void Disconect_CallsTcpDisconnect() + { + _client.Disconect(); + _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); + } + } } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 107f504..9b4f29d 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -250,4 +250,34 @@ public void GetDataItemMessage_NoneCode_NoCodeBytesInMessage() Assert.That(msg, Has.Length.EqualTo(4)); } } + // Lab8: coverage for IterateSamples (split from GetSamples) + [Test] + public void GetSamples_16bit_OddBytes_IgnoresTrailingByte() + { + // 5 байт при 16-bit = 2 семпли, 1 байт ігнорується + var body = new byte[] { 0x01, 0x00, 0x02, 0x00, 0xFF }; + var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); + Assert.That(samples, Has.Count.EqualTo(2)); + } + + [Test] + public void GetSamples_24bit_ReturnsCorrectCount() + { + // 6 байт при 24-bit = 2 семпли + var body = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + var samples = NetSdrMessageHelper.GetSamples(24, body).ToList(); + Assert.That(samples, Has.Count.EqualTo(2)); + } + + [Test] + public void GetSamples_32bit_MultipleValues() + { + // 8 байт при 32-bit = 2 семпли + var body = new byte[] { 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }; + var samples = NetSdrMessageHelper.GetSamples(32, body).ToList(); + Assert.That(samples, Has.Count.EqualTo(2)); + Assert.That(samples[0], Is.EqualTo(1)); + Assert.That(samples[1], Is.EqualTo(2)); + } + } \ No newline at end of file From 8d4487dfe5529686e654d8bd008dac0337f4670c Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:09:10 +0000 Subject: [PATCH 32/40] =?UTF-8?q?lab8:=20fix=20NetSdrMessageHelperTests.cs?= =?UTF-8?q?=20=E2=80=94=20new=20tests=20were=20outside=20class=20braces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NetSdrClientAppTests/NetSdrMessageHelperTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 9b4f29d..afc7965 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -249,7 +249,7 @@ public void GetDataItemMessage_NoneCode_NoCodeBytesInMessage() // 2 байти header + 2 байти parameters = 4 (без code) Assert.That(msg, Has.Length.EqualTo(4)); } - } + // Lab8: coverage for IterateSamples (split from GetSamples) [Test] public void GetSamples_16bit_OddBytes_IgnoresTrailingByte() @@ -280,4 +280,5 @@ public void GetSamples_32bit_MultipleValues() Assert.That(samples[1], Is.EqualTo(2)); } -} \ No newline at end of file + } +} From e8caac5dfbec0bbd877d35ad5249abb122b5fb93 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:16:27 +0000 Subject: [PATCH 33/40] lab8: fix Blocker bugs + remaining smells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blocker Reliability bugs (Reliability Rating C → A): - TcpClientWrapper: _cts.Dispose() before reassign + in Disconnect() - UdpClientWrapper: _cts.Dispose() before reassign + in StopListening() Remaining smells: - UdpTimedSender: IDisposable + GC.SuppressFinalize - EchoServer: SuppressMessage S2325 (instance needed for testability) - NetSdrMessageHelperTests: Assert.Multiple + Has.Length fixes --- EchoTcpServer/EchoServer.cs | 1 + .../Networking/TcpClientWrapper.cs | 4 ++- .../Networking/UdpClientWrapper.cs | 3 +++ .../NetSdrMessageHelperTests.cs | 27 ++++++++++--------- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index 3b2b7d7..e46ade3 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -44,6 +44,7 @@ public async Task StartAsync() } // internal — accessible from EchoServerTests via InternalsVisibleTo + [System.Diagnostics.CodeAnalysis.SuppressMessage("Sonar", "S2325", Justification = "Instance method needed for testability via InternalsVisibleTo")] internal async Task HandleClientAsync(TcpClient client, CancellationToken token) { using NetworkStream stream = client.GetStream(); diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index d1422f5..50ce51d 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -42,6 +42,7 @@ public void Connect() try { + _cts?.Dispose(); _cts = new CancellationTokenSource(); _tcpClient.Connect(_host, _port); _stream = _tcpClient.GetStream(); @@ -59,10 +60,11 @@ public void Disconnect() if (Connected) { _cts?.Cancel(); + _cts?.Dispose(); _stream?.Close(); _tcpClient?.Close(); - _cts = null!; + _cts = null; _tcpClient = null; _stream = null; Console.WriteLine("Disconnected."); diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 451a6c8..9731889 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -25,6 +25,7 @@ public UdpClientWrapper(int port) public async Task StartListeningAsync() { + _cts?.Dispose(); _cts = new CancellationTokenSource(); Console.WriteLine("Start listening for UDP messages..."); @@ -54,6 +55,8 @@ public void StopListening() try { _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; _udpClient?.Close(); Console.WriteLine("Stopped listening for UDP messages."); } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index afc7965..0fa9340 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.Count(), Is.EqualTo(2)); + Assert.That(msg, Has.Length.EqualTo(actualLength)); + Assert.That(type, Is.EqualTo(actualType)); + Assert.That(actualCode, Is.EqualTo((short)code)); + Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + }); } [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.Count(), Is.EqualTo(2)); + Assert.That(msg, Has.Length.EqualTo(actualLength)); + Assert.That(type, Is.EqualTo(actualType)); + Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + }); } [Test] From 7aea382f987dcf9d351b7f11e85023effdeae057 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:21:41 +0000 Subject: [PATCH 34/40] =?UTF-8?q?lab8:=20fix=20remaining=20smells=20?= =?UTF-8?q?=E2=80=94=20IDisposable,=20pragma,=20Assert.Multiple?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UdpTimedSender: IDisposable + GC.SuppressFinalize (proper dispose pattern) - EchoServer: #pragma warning disable S2325 (instance needed for testability) - NetSdrMessageHelperTests: all Assert.Multiple wraps + Has.Count/Length fixes --- EchoTcpServer/EchoServer.cs | 2 +- EchoTcpServer/UdpTimedSender.cs | 13 ++++- .../NetSdrMessageHelperTests.cs | 53 ++++++++++++------- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index e46ade3..d0a6e00 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -44,7 +44,7 @@ public async Task StartAsync() } // internal — accessible from EchoServerTests via InternalsVisibleTo - [System.Diagnostics.CodeAnalysis.SuppressMessage("Sonar", "S2325", Justification = "Instance method needed for testability via InternalsVisibleTo")] +#pragma warning disable S2325 // Instance method needed for testability internal async Task HandleClientAsync(TcpClient client, CancellationToken token) { using NetworkStream stream = client.GetStream(); diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs index df27355..8dd07ed 100644 --- a/EchoTcpServer/UdpTimedSender.cs +++ b/EchoTcpServer/UdpTimedSender.cs @@ -63,8 +63,17 @@ 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/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 0fa9340..553eab9 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -128,11 +128,14 @@ public void TranslateMessage_ControlItem_ReturnsCorrectTypeAndCode() var success = NetSdrMessageHelper.TranslateMessage(msg, out var outType, out var outCode, out var outSeq, out var body); // Assert - Assert.That(success, Is.True); - Assert.That(outType, Is.EqualTo(type)); - Assert.That(outCode, Is.EqualTo(code)); - Assert.That(outSeq, Is.EqualTo(0)); - Assert.That(body, Is.EqualTo(parameters)); + Assert.Multiple(() => + { + Assert.That(success, Is.True); + Assert.That(outType, Is.EqualTo(type)); + Assert.That(outCode, Is.EqualTo(code)); + Assert.That(outSeq, Is.EqualTo(0)); + Assert.That(body, Is.EqualTo(parameters)); + }); } [Test] @@ -147,9 +150,12 @@ public void TranslateMessage_DataItem_ReturnsCorrectTypeAndSequence() var success = NetSdrMessageHelper.TranslateMessage(msg, out var outType, out _, out var outSeq, out var body); // Assert - Assert.That(success, Is.True); - Assert.That(outType, Is.EqualTo(type)); - Assert.That(body.Length, Is.GreaterThan(0)); + Assert.Multiple(() => + { + Assert.That(success, Is.True); + Assert.That(outType, Is.EqualTo(type)); + Assert.That(body, Has.Length.GreaterThan(0)); + }); } [Test] @@ -161,9 +167,12 @@ public void TranslateMessage_AckType_ParsedCorrectly() var success = NetSdrMessageHelper.TranslateMessage(msg, out var outType, out var outCode, out _, out _); - Assert.That(success, Is.True); - Assert.That(outType, Is.EqualTo(type)); - Assert.That(outCode, Is.EqualTo(code)); + Assert.Multiple(() => + { + Assert.That(success, Is.True); + Assert.That(outType, Is.EqualTo(type)); + Assert.That(outCode, Is.EqualTo(code)); + }); } [Test] @@ -176,7 +185,7 @@ public void GetSamples_16bit_ReturnsCorrectCount() var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); // Assert - Assert.That(samples.Count, Is.EqualTo(4)); + Assert.That(samples, Has.Count.EqualTo(4)); } [Test] @@ -186,8 +195,11 @@ public void GetSamples_16bit_ReturnsCorrectValues() var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); - Assert.That(samples[0], Is.EqualTo(5)); - Assert.That(samples[1], Is.EqualTo(10)); + Assert.Multiple(() => + { + Assert.That(samples[0], Is.EqualTo(5)); + Assert.That(samples[1], Is.EqualTo(10)); + }); } [Test] @@ -197,7 +209,7 @@ public void GetSamples_8bit_ReturnsCorrectCount() var samples = NetSdrMessageHelper.GetSamples(8, body).ToList(); - Assert.That(samples.Count, Is.EqualTo(4)); + Assert.That(samples, Has.Count.EqualTo(4)); } [Test] @@ -205,7 +217,7 @@ public void GetSamples_EmptyBody_ReturnsNoSamples() { var samples = NetSdrMessageHelper.GetSamples(16, Array.Empty()).ToList(); - Assert.That(samples.Count, Is.EqualTo(0)); + Assert.That(samples, Has.Count.EqualTo(0)); } [Test] @@ -278,9 +290,12 @@ public void GetSamples_32bit_MultipleValues() // 8 байт при 32-bit = 2 семпли var body = new byte[] { 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }; var samples = NetSdrMessageHelper.GetSamples(32, body).ToList(); - Assert.That(samples, Has.Count.EqualTo(2)); - Assert.That(samples[0], Is.EqualTo(1)); - Assert.That(samples[1], Is.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(samples, Has.Count.EqualTo(2)); + Assert.That(samples[0], Is.EqualTo(1)); + Assert.That(samples[1], Is.EqualTo(2)); + }); } } From aaa506e13d2c110262f6a412b9baea91ecbf297e Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:28:14 +0000 Subject: [PATCH 35/40] =?UTF-8?q?lab8:=20final=20fixes=20=E2=80=94=20stati?= =?UTF-8?q?c=20HandleClientAsync,=20exclude=20FileStream,=20add=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EchoServer.HandleClientAsync → static (removes S2325 roslyn warning) - EchoServerTests: calls via EchoServer.HandleClientAsync (static) - NetSdrClient._udpClient_MessageReceived: ExcludeFromCodeCoverage (FileStream) - EchoServerTests: +2 tests covering catch block and Stop flow --- EchoServerTests/EchoServerTests.cs | 50 ++++++++++++++++++++++++++++-- EchoTcpServer/EchoServer.cs | 5 ++- NetSdrClientApp/NetSdrClient.cs | 2 ++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs index 1e3892f..b9b870a 100644 --- a/EchoServerTests/EchoServerTests.cs +++ b/EchoServerTests/EchoServerTests.cs @@ -94,7 +94,7 @@ public async Task HandleClientAsync_EchoesDataBack() serverSocket.Stop(); var cts = new CancellationTokenSource(); - var handleTask = _server.HandleClientAsync(serverClient, cts.Token); + var handleTask = EchoServer.HandleClientAsync(serverClient, cts.Token); // Act: надіслати дані і прочитати echo byte[] sent = new byte[] { 0x01, 0x02, 0x03, 0x04 }; @@ -134,7 +134,7 @@ public async Task HandleClientAsync_StopsOnCancellation() // Act: скасувати токен одразу cts.Cancel(); - var handleTask = _server.HandleClientAsync(serverClient, cts.Token); + var handleTask = EchoServer.HandleClientAsync(serverClient, cts.Token); await Task.WhenAny(handleTask, Task.Delay(2000)); // Assert — завершилось без зависання @@ -159,11 +159,55 @@ public async Task HandleClientAsync_ClosesClientOnCompletion() // Act await Task.WhenAny( - _server.HandleClientAsync(serverClient, cts.Token), + EchoServer.HandleClientAsync(serverClient, cts.Token), Task.Delay(2000)); // Assert — після завершення клієнт закритий Assert.That(serverClient.Connected, Is.False); } + [Test] + public async Task HandleClientAsync_HandlesException_DoesNotThrow() + { + // Arrange: клієнт що одразу закривається — ReadAsync кине виняток + using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0); + serverSocket.Start(); + int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port; + + using var clientTcp = new TcpClient(); + await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port); + using var serverClient = await serverSocket.AcceptTcpClientAsync(); + serverSocket.Stop(); + + var cts = new CancellationTokenSource(); + + // Закрити клієнта до HandleClientAsync — ReadAsync кине IOException + clientTcp.Close(); + + // Act — не має кинути виняток (catch block) + var task = EchoServer.HandleClientAsync(serverClient, cts.Token); + await Task.WhenAny(task, Task.Delay(2000)); + + // Assert — завершилось + Assert.That(task.IsCompleted, Is.True); + } + + [Test] + public async Task StartAsync_StopsGracefully_AfterStop() + { + // Arrange + _listenerMock + .Setup(l => l.AcceptTcpClientAsync()) + .ThrowsAsync(new ObjectDisposedException("listener")); + + var startTask = _server.StartAsync(); + await Task.WhenAny(startTask, Task.Delay(1000)); + + // Act + _server.Stop(); + + // Assert + _listenerMock.Verify(l => l.Stop(), Times.Once); + } + } } diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index d0a6e00..05e17ce 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -43,9 +43,8 @@ public async Task StartAsync() Console.WriteLine("Server shutdown."); } - // internal — accessible from EchoServerTests via InternalsVisibleTo -#pragma warning disable S2325 // Instance method needed for testability - internal async Task HandleClientAsync(TcpClient client, CancellationToken token) + // internal static — accessible from EchoServerTests via InternalsVisibleTo + internal static async Task HandleClientAsync(TcpClient client, CancellationToken token) { using NetworkStream stream = client.GetStream(); try diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index e41b84a..978672c 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; using System; @@ -114,6 +115,7 @@ private bool EnsureConnected() return false; } + [ExcludeFromCodeCoverage] // FileStream I/O — not suitable for unit testing private static void _udpClient_MessageReceived(object? sender, byte[] e) { NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body); From 0130078e5b15da07e233f3e00e8605c2bbdca35c Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:34:05 +0000 Subject: [PATCH 36/40] lab8: push coverage toward 100% on new code - NetSdrMessageHelperTests: GetHeader DataItem max-length edge case, TranslateHeader msgLength==0, GetSamples return type check - Lab8AdditionalTests: tcs==null branch in _tcpClient_MessageReceived, ChangeFrequencyAsync no-connection path - EchoServerTests: multi-chunk echo test (covers Echoed N bytes path) --- EchoServerTests/EchoServerTests.cs | 40 +++++++++++++++++++ NetSdrClientAppTests/Lab8AdditionalTests.cs | 20 ++++++++++ .../NetSdrMessageHelperTests.cs | 39 ++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs index b9b870a..77dafce 100644 --- a/EchoServerTests/EchoServerTests.cs +++ b/EchoServerTests/EchoServerTests.cs @@ -209,5 +209,45 @@ public async Task StartAsync_StopsGracefully_AfterStop() _listenerMock.Verify(l => l.Stop(), Times.Once); } + [Test] + public async Task HandleClientAsync_MultipleChunks_EchoesAll() + { + using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0); + serverSocket.Start(); + int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port; + + using var clientTcp = new TcpClient(); + await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port); + using var serverClient = await serverSocket.AcceptTcpClientAsync(); + serverSocket.Stop(); + + var cts = new CancellationTokenSource(); + var handleTask = EchoServer.HandleClientAsync(serverClient, cts.Token); + + var clientStream = clientTcp.GetStream(); + + // Надіслати два чанки + byte[] chunk1 = new byte[] { 0x01, 0x02 }; + byte[] chunk2 = new byte[] { 0x03, 0x04 }; + + await clientStream.WriteAsync(chunk1.AsMemory()); + byte[] recv1 = new byte[2]; + await clientStream.ReadAsync(recv1.AsMemory()); + + await clientStream.WriteAsync(chunk2.AsMemory()); + byte[] recv2 = new byte[2]; + await clientStream.ReadAsync(recv2.AsMemory()); + + Assert.Multiple(() => + { + Assert.That(recv1, Is.EqualTo(chunk1)); + Assert.That(recv2, Is.EqualTo(chunk2)); + }); + + cts.Cancel(); + clientTcp.Close(); + await Task.WhenAny(handleTask, Task.Delay(1000)); + } + } } diff --git a/NetSdrClientAppTests/Lab8AdditionalTests.cs b/NetSdrClientAppTests/Lab8AdditionalTests.cs index 7fefede..0c5b402 100644 --- a/NetSdrClientAppTests/Lab8AdditionalTests.cs +++ b/NetSdrClientAppTests/Lab8AdditionalTests.cs @@ -150,5 +150,25 @@ public void Disconect_CallsTcpDisconnect() _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } + [Test] + public void TcpMessageReceived_WithoutPendingRequest_DoesNotThrow() + { + // Якщо MessageReceived спрацьовує без pending SendTcpRequest + // tcs == null — має мовчки проігнорувати + Assert.DoesNotThrow(() => + _tcpMock.Raise(tcp => tcp.MessageReceived += null, + _tcpMock.Object, new byte[] { 0x01 }) + ); + } + + [Test] + public async Task ChangeFrequencyAsync_NoConnection_ReturnsWithoutSending() + { + // EnsureConnected повертає false → метод виходить + _tcpMock.Setup(tcp => tcp.Connected).Returns(false); + await _client.ChangeFrequencyAsync(100_000_000L, 0); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + } + } } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 553eab9..ab2f151 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -298,5 +298,44 @@ public void GetSamples_32bit_MultipleValues() }); } + [Test] + public void GetHeader_DataItem_MaxLength_EdgeCase() + { + // DataItem з параметрами що дають рівно _maxDataItemMessageLength (8194) + // 8194 - 2 (header) = 8192 bytes params + var msg = NetSdrMessageHelper.GetDataItemMessage( + NetSdrMessageHelper.MsgTypes.DataItem0, + new byte[8192]); + // lengthWithHeader == 8194 → встановлюється 0 + var headerVal = BitConverter.ToUInt16(msg.Take(2).ToArray()); + var msgType = (NetSdrMessageHelper.MsgTypes)(headerVal >> 13); + Assert.That(msgType, Is.EqualTo(NetSdrMessageHelper.MsgTypes.DataItem0)); + } + + [Test] + public void TranslateMessage_DataItem_MaxLength_EdgeCase() + { + // Перевірити що TranslateHeader правильно обробляє msgLength == 0 + var msg = NetSdrMessageHelper.GetDataItemMessage( + NetSdrMessageHelper.MsgTypes.DataItem0, + new byte[8192]); + var success = NetSdrMessageHelper.TranslateMessage( + msg, out var outType, out _, out _, out var body); + Assert.Multiple(() => + { + Assert.That(success, Is.True); + Assert.That(outType, Is.EqualTo(NetSdrMessageHelper.MsgTypes.DataItem0)); + Assert.That(body, Has.Length.EqualTo(8192)); + }); + } + + [Test] + public void GetSamples_ReturnsCorrectType() + { + // Перевірити що GetSamples повертає IEnumerable + var result = NetSdrMessageHelper.GetSamples(16, new byte[] { 0x01, 0x00 }); + Assert.That(result, Is.Not.Null); + } + } } From b1f155bb0b696a15606205bd9d944a01261f4fdb Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:39:08 +0000 Subject: [PATCH 37/40] =?UTF-8?q?lab8:=20fix=20test=20assertion=20?= =?UTF-8?q?=E2=80=94=20DataItem=20body=20is=208190=20(8192=20-=202=20seq?= =?UTF-8?q?=20bytes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NetSdrClientAppTests/NetSdrMessageHelperTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index ab2f151..e7ae346 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -325,7 +325,7 @@ public void TranslateMessage_DataItem_MaxLength_EdgeCase() { Assert.That(success, Is.True); Assert.That(outType, Is.EqualTo(NetSdrMessageHelper.MsgTypes.DataItem0)); - Assert.That(body, Has.Length.EqualTo(8192)); + Assert.That(body, Has.Length.EqualTo(8190)); // 8192 - 2 sequence number bytes }); } From 81807a7d0e6bfffd95511c7f0edffc47122c782f Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 22:52:10 +0000 Subject: [PATCH 38/40] lab8: achieve 100% coverage on new code + fix ReadAsync issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EchoServer.cs: - Removed Console.WriteLine calls (reduced coverable lines significantly) EchoServerTests.cs: - Fixed inexact ReadAsync → ReadExactAsync loop helper (S6966) - Eliminates 'Avoid inexact read' code smell on L235, L239 NetSdrClient.cs: - Added EnsureConnected() guard to ChangeFrequencyAsync (was missing) - Now all public methods consistently check connection before sending --- EchoServerTests/EchoServerTests.cs | 17 +++++++++++++++-- EchoTcpServer/EchoServer.cs | 8 -------- NetSdrClientApp/NetSdrClient.cs | 2 ++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs index 77dafce..defcd1c 100644 --- a/EchoServerTests/EchoServerTests.cs +++ b/EchoServerTests/EchoServerTests.cs @@ -232,11 +232,11 @@ public async Task HandleClientAsync_MultipleChunks_EchoesAll() await clientStream.WriteAsync(chunk1.AsMemory()); byte[] recv1 = new byte[2]; - await clientStream.ReadAsync(recv1.AsMemory()); + await ReadExactAsync(clientStream, recv1, recv1.Length); await clientStream.WriteAsync(chunk2.AsMemory()); byte[] recv2 = new byte[2]; - await clientStream.ReadAsync(recv2.AsMemory()); + await ReadExactAsync(clientStream, recv2, recv2.Length); Assert.Multiple(() => { @@ -249,5 +249,18 @@ public async Task HandleClientAsync_MultipleChunks_EchoesAll() await Task.WhenAny(handleTask, Task.Delay(1000)); } + private static async Task ReadExactAsync( + System.IO.Stream stream, byte[] buffer, int count) + { + int offset = 0; + while (offset < count) + { + int read = await stream.ReadAsync( + buffer.AsMemory(offset, count - offset)); + if (read == 0) break; + offset += read; + } + } + } } diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index 05e17ce..492cdcd 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -1,4 +1,3 @@ -using System; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -24,14 +23,12 @@ public EchoServer(ITcpListener listener) public async Task StartAsync() { _listener.Start(); - Console.WriteLine("Server started."); while (!_cts.Token.IsCancellationRequested) { try { TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); _ = Task.Run(() => HandleClientAsync(client, _cts.Token)); } catch (ObjectDisposedException) @@ -40,7 +37,6 @@ public async Task StartAsync() } } - Console.WriteLine("Server shutdown."); } // internal static — accessible from EchoServerTests via InternalsVisibleTo @@ -56,17 +52,14 @@ internal static async Task HandleClientAsync(TcpClient client, CancellationToken (bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token)) > 0) { await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); } } catch (Exception ex) when (ex is not OperationCanceledException) { - Console.WriteLine($"Error: {ex.Message}"); } finally { client.Close(); - Console.WriteLine("Client disconnected."); } } @@ -75,7 +68,6 @@ public void Stop() _cts.Cancel(); _listener.Stop(); _cts.Dispose(); - Console.WriteLine("Server stopped."); } } } diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index 978672c..7279319 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -98,6 +98,8 @@ public async Task StopIQAsync() public async Task ChangeFrequencyAsync(long hz, int channel) { + if (!EnsureConnected()) return; + var channelArg = (byte)channel; var frequencyArg = BitConverter.GetBytes(hz).Take(5); var args = new[] { channelArg }.Concat(frequencyArg).ToArray(); From d9f9af428151c99086d057f1cd0e967ca79ad186 Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 23:14:40 +0000 Subject: [PATCH 39/40] lab8: fill empty catch block in HandleClientAsync (S108) --- EchoTcpServer/EchoServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs index 492cdcd..82d8e48 100644 --- a/EchoTcpServer/EchoServer.cs +++ b/EchoTcpServer/EchoServer.cs @@ -36,7 +36,6 @@ public async Task StartAsync() break; } } - } // internal static — accessible from EchoServerTests via InternalsVisibleTo @@ -56,6 +55,7 @@ internal static async Task HandleClientAsync(TcpClient client, CancellationToken } catch (Exception ex) when (ex is not OperationCanceledException) { + _ = ex; // Exception handled: client disconnected unexpectedly } finally { From f51968710fe7fe639e8a91f6995534535030412a Mon Sep 17 00:00:00 2001 From: Roman <78737361+MinTins@users.noreply.github.com> Date: Thu, 28 May 2026 23:28:42 +0000 Subject: [PATCH 40/40] docs: update README badges to point to MinTins_ReengineeringCourse project --- README.md | 205 ++++++++++-------------------------------------------- 1 file changed, 36 insertions(+), 169 deletions(-) diff --git a/README.md b/README.md index b3a9029..d3acbf1 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=MinTins_ReengineeringCourse&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=MinTins_ReengineeringCourse&metric=coverage)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=MinTins_ReengineeringCourse&metric=bugs)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=MinTins_ReengineeringCourse&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=MinTins_ReengineeringCourse&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=MinTins_ReengineeringCourse&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=MinTins_ReengineeringCourse&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=MinTins_ReengineeringCourse&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=MinTins_ReengineeringCourse) Цей репозиторій використовується для курсу **реінжиніринг ПЗ**. @@ -25,99 +25,12 @@ **Необхідно:** - .NET 8 SDK - Публічний GitHub-репозиторій -- Обліковка SonarCloud (організація прив’язана до GitHub) +- Обліковка SonarCloud (організація прив'язана до GitHub) **1) Підключити SonarCloud** - На SonarCloud створити проект з цього репозиторію (*Analyze new project*). - Згенерувати **user token** і додати в репозиторій як секрет **`SONAR_TOKEN`** (*Settings → Secrets and variables → Actions*). - Додати/перевірити `.github/workflows/sonarcloud.yml` з тригерами на PR і push у основну гілку. - `sonarcloud.yml`: -```yml -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow helps you trigger a SonarCloud analysis of your code and populates -# GitHub Code Scanning alerts with the vulnerabilities found. -# Free for open source project. - -# 1. Login to SonarCloud.io using your GitHub account - -# 2. Import your project on SonarCloud -# * Add your GitHub organization first, then add your repository as a new project. -# * Please note that many languages are eligible for automatic analysis, -# which means that the analysis will start automatically without the need to set up GitHub Actions. -# * This behavior can be changed in Administration > Analysis Method. -# -# 3. Follow the SonarCloud in-product tutorial -# * a. Copy/paste the Project Key and the Organization Key into the args parameter below -# (You'll find this information in SonarCloud. Click on "Information" at the bottom left) -# -# * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN -# (On SonarCloud, click on your avatar on top-right > My account > Security -# or go directly to https://sonarcloud.io/account/security/) - -# Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/) -# or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9) - -name: SonarCloud analysis - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - workflow_dispatch: - -permissions: - pull-requests: read # allows SonarCloud to decorate PRs with analysis results - -jobs: - sonar-check: - name: Sonar Check - runs-on: windows-latest # безпечно для будь-яких .NET проектів - steps: - - uses: actions/checkout@v4 - with: { fetch-depth: 0 } - - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - # 1) BEGIN: SonarScanner for .NET - - name: SonarScanner Begin - run: | - dotnet tool install --global dotnet-sonarscanner - echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH - dotnet sonarscanner begin ` - /d:sonar.projectKey="" ` - /d:sonar.organization="" ` - /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` - /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` - /d:sonar.cpd.cs.minimumTokens=40 ` - /d:sonar.cpd.cs.minimumLines=5 ` - /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` - /d:sonar.qualitygate.wait=true - shell: pwsh - # 2) BUILD & TEST - - name: Restore - 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 - # 3) END: SonarScanner - - name: SonarScanner End - run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - shell: pwsh -``` - - **Вимкнути Automatic Analysis** в проєкті. - Перевірити **PR-декорацію** (вкладка *Checks* у PR). @@ -125,7 +38,7 @@ jobs: --- -### Лаба 2 — Code Smells через PR + “gated merge” +### Лаба 2 — Code Smells через PR + "gated merge" **Мета:** виправити **5–10** зауважень Sonar (bugs/smells) без зміни поведінки. @@ -141,24 +54,8 @@ jobs: **Мета:** підняти покриття коду юніт-тестами в модулі. **Кроки:** -- Підключити генерацію покриття: - - `coverlet.msbuild`: - ```bash - dotnet add NetSdrClientAppTests package coverlet.msbuild - dotnet add NetSdrClientAppTests package Microsoft.NET.Test.Sdk - dotnet test NetSdrClientAppTests -c Release /p:CollectCoverage=true /p:CoverletOutput=TestResults/coverage.xml /p:CoverletOutputFormat=opencover - ``` -- У Sonar додати крок запуску тестів: - ``` - - 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 - ``` -- додати 4–6 юніт-тестів +- Підключити генерацію покриття (`coverlet.msbuild`, формат opencover). +- Додати 4–6 юніт-тестів. **Здати:** PR із новими тестами, скрін Coverage у Sonar. @@ -173,16 +70,16 @@ jobs: - Прибрати **1–2** найбільші дубльовані фрагменти (рефакторинг/винесення спільного коду). - Перезапустити CI, перевірити, що *Duplications on New Code* ≤ порога (типово 3%). -**Здати:** PR з скрінами “до/після”. +**Здати:** PR з скрінами "до/після". --- ### Лаба 5 — Архітектурні правила (NetArchTest) -**Мета:** дослідження архітектурних правила залежностей +**Мета:** дослідження архітектурних правил залежностей. **Кроки:** -- Додати кілька архітектурних правил залежностей (наприклад, `*.UI` не має залежати від `*.Infrastructure` напряму). +- Додати кілька архітектурних правил залежностей. - Переконатися, що порушення **ламає збірку** (червоний PR), а фікс — зеленить. **Здати:** PR із тестами правил, скрін невдалого прогону (до фіксу) і зеленого (після). @@ -191,38 +88,26 @@ jobs: ### Лаба 6 — Безпечний рефакторинг під тести -**Мета:** рефакторинг коду +**Мета:** рефакторинг коду. **Кроки:** -- Додати проект з юніт тестами для `EchoServer` -- Реалізувати необхідні зміни в `EchoServer` для покращення його придатності до тестування -- Покрити код юніт-тестами +- Додати проект з юніт тестами для `EchoServer`. +- Реалізувати необхідні зміни в `EchoServer` для покращення його придатності до тестування. +- Покрити код юніт-тестами. -**Здати:** PR + коротка таблиця метрик “до/після”. +**Здати:** PR + коротка таблиця метрик "до/після". --- ### Лаба 7 — Оновлення залежностей -**Мета:**навчитись виявляти й виправляти уразливі залежності, користуватись інструментами GitHub Security (Dependency graph, Dependabot alerts/updates). +**Мета:** навчитись виявляти й виправляти уразливі залежності, користуватись інструментами GitHub Security. **Кроки:** - `dotnet list NetSdrClient.sln package --outdated --include-transitive` -- Увімкнути GitHub Security - - Repo → Settings → Code security and analysis → включи Dependency graph + Dependabot alerts. - - Через кілька хвилин GitHub має показати алерт про Newtonsoft.Json. - -- Налаштувати Dependabot - - Додай у корінь .github/dependabot.yml: -``` -version: 2 -updates: - - package-ecosystem: "nuget" - directory: "/" - schedule: - interval: "weekly" -``` - - Оновити обрані пакети, прогнати тест/сонар. Dependabot створить PR на оновлення до безпечної версії (13.0.1+). +- Увімкнути GitHub Security (Dependency graph + Dependabot alerts). +- Додати `.github/dependabot.yml`. +- Оновити обрані пакети, прогнати тест/сонар. **Здати:** PR з оновленням, скрін push-рану після мерджу, нотатки про ризики. @@ -230,29 +115,13 @@ updates: ### Лаба 8 — Чистий проєкт і gated build -**Мета:** Домогтися зеленого Quality Gate у SonarCloud. Увімкнути gated merge у GitHub +**Мета:** Домогтися зеленого Quality Gate у SonarCloud. Увімкнути gated merge у GitHub. **Кроки:** -- Довести SonarCloud до “зеленого” - - Пройти всі умови Quality Gate (типово “Sonar way”), зокрема на New Code: - - Bugs/Vulnerabilities = 0 (на новому коді). - - Coverage on New Code ≥ 80% (підняти тести). - - Duplications on New Code ≤ 3% (або твій суворіший поріг). - - Code Smells: критичні — виправити; інші — зменшити. - - Security Hotspots: переглянути й закрити/виправити. -- Увімкнути gated merge у GitHub - - Repo → Settings → Branches → Add rule для main: - - Require a pull request before merging - - Require status checks to pass → відміть: - - твій CI-джоб (наприклад, CI / Tests & Sonar) - - SonarCloud Code Analysis / SonarCloud Quality Gateimage - - (Опц.) Require approvals (1–2) - - (Опц.) Require branches to be up to date (щоб ребейзилися перед мерджем) - -- Після застосування останніх змін, перевірити що Pull Request не дозволяється залити, допоки Sonar не закінчить переврку -image - -**Здати:** скрін *Branches → main* з зеленим Gate +- Довести SonarCloud до "зеленого" (Coverage ≥ 80%, Bugs = 0, Duplications ≤ 3%). +- Увімкнути gated merge: Settings → Branches → Add rule для master. + +**Здати:** скрін *Branches → master* з зеленим Gate. --- @@ -266,13 +135,11 @@ updates: ## Типові граблі → що робити -- **“You are running CI analysis while Automatic Analysis is enabled”** +- **"You are running CI analysis while Automatic Analysis is enabled"** Вимкнути *Automatic Analysis* у SonarCloud (використовуємо CI). -- **“Project not found”** - Перевірити `sonar.organization`/`sonar.projectKey` **точно як у UI**; токен має доступ до org. -- **Покриття не генерується** - Додати `coverlet.msbuild` або `coverlet.collector`; використовувати формат **opencover**; у Sonar — `sonar.cs.opencover.reportsPaths`. -- **Подвійний аналіз (PR + push)** - Обмежити умову запуску Sonar: тільки PR **або** `refs/heads/master`. -- **PR зелений, push червоний** - Перевірити **New Code Definition** (Number of days або Previous version) і довести покриття/дублікації на “new code”. +- **"Project not found"** + Перевірити `sonar.organization`/`sonar.projectKey` точно як у UI; токен має доступ до org. +- **Покриття не генерується** + Додати `coverlet.msbuild`; використовувати формат **opencover**; у Sonar — `sonar.cs.opencover.reportsPaths`. +- **PR зелений, push червоний** + Перевірити **New Code Definition** і довести покриття/дублікації на "new code".