diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index e7840696..bdd86f6c 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -1,83 +1,53 @@
-# 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
+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 проектів
+ sonar:
+ runs-on: windows-latest
steps:
+
- uses: actions/checkout@v4
- with: { fetch-depth: 0 }
+ with:
+ fetch-depth: 0
- - uses: actions/setup-dotnet@v4
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- # 1) BEGIN: SonarScanner for .NET
+ - name: Restore
+ run: dotnet restore NetSdrClient.sln
+
+ - name: Build
+ run: dotnet build NetSdrClient.sln -c Release --no-restore
+
+ - name: Run tests with coverage
+ run: |
+ dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release `
+ /p:CollectCoverage=true `
+ /p:CoverletOutput=TestResults/coverage.xml `
+ /p:CoverletOutputFormat=opencover
+ shell: pwsh
+
- name: SonarScanner Begin
run: |
dotnet tool install --global dotnet-sonarscanner
echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH
dotnet sonarscanner begin `
- /k:"ppanchen_NetSdrClient" `
- /o:"ppanchen" `
- /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
+ /k:"olekca160406_NetSdrClient" `
+ /o:"olekca160406" `
+ /d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
+ /d:sonar.cs.opencover.reportsPaths="**/coverage.xml"
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: Rebuild for Sonar
+ run: dotnet build NetSdrClient.sln -c Release
+
- name: SonarScanner End
run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
shell: pwsh
diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs
index 5966c579..9affa28f 100644
--- a/EchoTcpServer/Program.cs
+++ b/EchoTcpServer/Program.cs
@@ -1,108 +1,107 @@
-using System;
+using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+
///
/// This program was designed for test purposes only
/// Not for a review
///
-public class EchoServer
+namespace NetSdrClientApp.EchoServer
{
- private readonly int _port;
- private TcpListener _listener;
- private CancellationTokenSource _cancellationTokenSource;
-
-
- public EchoServer(int port)
+ public class EchoServer
{
- _port = port;
- _cancellationTokenSource = new CancellationTokenSource();
- }
+ private readonly int _port;
+ private TcpListener _listener;
+ private readonly CancellationTokenSource _cancellationTokenSource;
- public async Task StartAsync()
- {
- _listener = new TcpListener(IPAddress.Any, _port);
- _listener.Start();
- Console.WriteLine($"Server started on port {_port}.");
+ public EchoServer(int port)
+ {
+ _port = port;
+ _cancellationTokenSource = new CancellationTokenSource();
+ }
- while (!_cancellationTokenSource.Token.IsCancellationRequested)
+ public async Task StartAsync()
{
- try
- {
- TcpClient client = await _listener.AcceptTcpClientAsync();
- Console.WriteLine("Client connected.");
+ _listener = new TcpListener(IPAddress.Any, _port);
+ _listener.Start();
+ Console.WriteLine($"Server started on port {_port}.");
- _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token));
- }
- catch (ObjectDisposedException)
+ while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
- // Listener has been closed
- break;
+ 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.");
- }
+ Console.WriteLine("Server shutdown.");
+ }
- private async Task HandleClientAsync(TcpClient client, CancellationToken token)
- {
- using (NetworkStream stream = client.GetStream())
+ private async Task HandleClientAsync(TcpClient client, CancellationToken token)
{
- try
+ using (NetworkStream stream = client.GetStream())
{
- byte[] buffer = new byte[8192];
- int bytesRead;
-
- while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
+ try
{
- // Echo back the received message
- await stream.WriteAsync(buffer, 0, bytesRead, token);
- Console.WriteLine($"Echoed {bytesRead} bytes to the client.");
+ 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.");
}
- }
- 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 void Stop()
+ {
+ _cancellationTokenSource.Cancel();
+ _listener.Stop();
+ _cancellationTokenSource.Dispose();
+ Console.WriteLine("Server stopped.");
+ }
- public static async Task Main(string[] args)
- {
- EchoServer server = new EchoServer(5000);
+ public static async Task Main(string[] args)
+ {
+ EchoServer server = new EchoServer(5000);
- // Start the server in a separate task
- _ = Task.Run(() => server.StartAsync());
+ _ = Task.Run(() => server.StartAsync());
- string host = "127.0.0.1"; // Target IP
- int port = 60000; // Target Port
- int intervalMilliseconds = 5000; // Send every 3 seconds
+ string host = "127.0.0.1";
+ int port = 60000;
+ int intervalMilliseconds = 5000;
- using (var sender = new UdpTimedSender(host, port))
- {
+ using var sender = new UdpTimedSender(host, port);
Console.WriteLine("Press any key to stop sending...");
sender.StartSending(intervalMilliseconds);
Console.WriteLine("Press 'q' to quit...");
while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q)
{
- // Just wait until 'q' is pressed
}
sender.StopSending();
@@ -110,64 +109,66 @@ public static async Task Main(string[] args)
Console.WriteLine("Sender stopped.");
}
}
-}
-
-
-public class UdpTimedSender : IDisposable
-{
- private readonly string _host;
- private readonly int _port;
- private readonly UdpClient _udpClient;
- private Timer _timer;
- public UdpTimedSender(string host, int port)
+ public class UdpTimedSender : IDisposable
{
- _host = host;
- _port = port;
- _udpClient = new UdpClient();
- }
+ private readonly string _host;
+ private readonly int _port;
+ private readonly UdpClient _udpClient;
+ private Timer? _timer;
- public void StartSending(int intervalMilliseconds)
- {
- if (_timer != null)
- throw new InvalidOperationException("Sender is already running.");
+ private ushort _counter;
- _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds);
- }
+ public UdpTimedSender(string host, int port)
+ {
+ _host = host;
+ _port = port;
+ _udpClient = new UdpClient();
+ }
- ushort i = 0;
+ public void StartSending(int intervalMilliseconds)
+ {
+ if (_timer != null)
+ throw new InvalidOperationException("Sender is already running.");
- private void SendMessageCallback(object state)
- {
- try
+ _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds);
+ }
+
+ private void SendMessageCallback(object? state)
{
- //dummy data
- Random rnd = new Random();
- byte[] samples = new byte[1024];
- rnd.NextBytes(samples);
- i++;
+ try
+ {
+ var 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();
- byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray();
- var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port);
+ var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port);
- _udpClient.Send(msg, msg.Length, endpoint);
- Console.WriteLine($"Message sent to {_host}:{_port} ");
+ _udpClient.Send(msg, msg.Length, endpoint);
+ Console.WriteLine($"Message sent to {_host}:{_port} ");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error sending message: {ex.Message}");
+ }
}
- catch (Exception ex)
+
+ public void StopSending()
{
- Console.WriteLine($"Error sending message: {ex.Message}");
+ _timer?.Dispose();
+ _timer = null;
}
- }
-
- public void StopSending()
- {
- _timer?.Dispose();
- _timer = null;
- }
- public void Dispose()
- {
- StopSending();
- _udpClient.Dispose();
+ public void Dispose()
+ {
+ StopSending();
+ _udpClient.Dispose();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs
index 1b9f9311..fb296806 100644
--- a/NetSdrClientApp/Networking/IUdpClient.cs
+++ b/NetSdrClientApp/Networking/IUdpClient.cs
@@ -1,10 +1,10 @@
-
-public interface IUdpClient
+namespace NetSdrClientApp.Networking
{
- event EventHandler? MessageReceived;
-
- Task StartListeningAsync();
-
- void StopListening();
- void Exit();
-}
\ No newline at end of file
+ public interface IUdpClient
+ {
+ event EventHandler? MessageReceived;
+ Task StartListeningAsync();
+ void StopListening();
+ void Exit();
+ }
+}
diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs
index 31e0b798..20bf1db3 100644
--- a/NetSdrClientApp/Networking/UdpClientWrapper.cs
+++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs
@@ -1,85 +1,98 @@
-using System;
+using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using NetSdrClientApp.Networking;
-public class UdpClientWrapper : IUdpClient
+namespace NetSdrClientApp.Networking
{
- private readonly IPEndPoint _localEndPoint;
- private CancellationTokenSource? _cts;
- private UdpClient? _udpClient;
-
- public event EventHandler? MessageReceived;
-
- public UdpClientWrapper(int port)
+ public class UdpClientWrapper : IUdpClient, IDisposable
{
- _localEndPoint = new IPEndPoint(IPAddress.Any, port);
- }
+ private readonly IPEndPoint _localEndPoint;
+ private CancellationTokenSource? _cts;
+ private UdpClient? _udpClient;
+ private bool _disposed;
- public async Task StartListeningAsync()
- {
- _cts = new CancellationTokenSource();
- Console.WriteLine("Start listening for UDP messages...");
+ public event EventHandler? MessageReceived;
- try
+ public UdpClientWrapper(int port)
{
- _udpClient = new UdpClient(_localEndPoint);
- while (!_cts.Token.IsCancellationRequested)
+ _localEndPoint = new IPEndPoint(IPAddress.Any, port);
+ }
+
+ public async Task StartListeningAsync()
+ {
+ _cts = new CancellationTokenSource();
+ Console.WriteLine("Start listening for UDP messages...");
+
+ try
{
- UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token);
- MessageReceived?.Invoke(this, result.Buffer);
+ _udpClient = new UdpClient(_localEndPoint);
+
+ while (!_cts.Token.IsCancellationRequested)
+ {
+ UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token);
+ MessageReceived?.Invoke(this, result.Buffer);
- Console.WriteLine($"Received from {result.RemoteEndPoint}");
+ Console.WriteLine($"Received from {result.RemoteEndPoint}");
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Console.WriteLine("Listening cancelled.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error receiving message: {ex.Message}");
}
}
- catch (OperationCanceledException ex)
- {
- //empty
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Error receiving message: {ex.Message}");
- }
- }
- public void StopListening()
- {
- try
+ public void StopListening()
{
_cts?.Cancel();
_udpClient?.Close();
Console.WriteLine("Stopped listening for UDP messages.");
}
- catch (Exception ex)
+
+ public void Exit()
{
- Console.WriteLine($"Error while stopping: {ex.Message}");
+ StopListening();
}
- }
- public void Exit()
- {
- try
+ public override bool Equals(object? obj)
{
- _cts?.Cancel();
- _udpClient?.Close();
- Console.WriteLine("Stopped listening for UDP messages.");
+ if (obj is not UdpClientWrapper other) return false;
+
+ return _localEndPoint.Port == other._localEndPoint.Port &&
+ _localEndPoint.Address.Equals(other._localEndPoint.Address);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(_localEndPoint.Address, _localEndPoint.Port);
}
- catch (Exception ex)
+
+ public void Dispose()
{
- Console.WriteLine($"Error while stopping: {ex.Message}");
+ Dispose(true);
+ GC.SuppressFinalize(this);
}
- }
- public override int GetHashCode()
- {
- var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}";
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return;
- using var md5 = MD5.Create();
- var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload));
+ if (disposing)
+ {
+ _cts?.Cancel();
+ _cts?.Dispose();
+ _udpClient?.Dispose();
+ }
- return BitConverter.ToInt32(hash, 0);
+ _disposed = true;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj
index 3cbc46af..4ee4be0c 100644
--- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj
+++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj
@@ -25,5 +25,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs
index ad00c4f8..1eddaecf 100644
--- a/NetSdrClientAppTests/NetSdrClientTests.cs
+++ b/NetSdrClientAppTests/NetSdrClientTests.cs
@@ -116,4 +116,61 @@ public async Task StopIQTest()
}
//TODO: cover the rest of the NetSdrClient code here
+ [Test]
+ public void GetControlItemMessage_CreatesValidMessage()
+ {
+ var msg = NetSdrMessageHelper.GetControlItemMessage(
+ NetSdrMessageHelper.MsgTypes.SetControlItem,
+ NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency,
+ new byte[] { 0x01, 0x02 });
+
+ Assert.That(msg.Length, Is.GreaterThan(4));
+ Assert.That(msg[2], Is.EqualTo(0x20)); // low byte of ReceiverFrequency
+ }
+
+ [Test]
+ public void TranslateMessage_ControlItem_WorksCorrectly()
+ {
+ var raw = NetSdrMessageHelper.GetControlItemMessage(
+ NetSdrMessageHelper.MsgTypes.SetControlItem,
+ NetSdrMessageHelper.ControlItemCodes.RFFilter,
+ new byte[] { 0xAA });
+
+ bool ok = NetSdrMessageHelper.TranslateMessage(
+ raw, out var type, out var code, out var seq, out var body);
+
+ Assert.IsTrue(ok);
+ Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.SetControlItem));
+ Assert.That(code, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.RFFilter));
+ Assert.That(body[0], Is.EqualTo(0xAA));
+ }
+ [Test]
+ public void TranslateMessage_DataItem_WorksCorrectly()
+ {
+ var raw = NetSdrMessageHelper.GetDataItemMessage(
+ NetSdrMessageHelper.MsgTypes.DataItem1,
+ new byte[] { 0x10, 0x20 });
+
+ bool ok = NetSdrMessageHelper.TranslateMessage(
+ raw, out var type, out var code, out var seq, out var body);
+
+ Assert.IsTrue(ok);
+ Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.DataItem1));
+ Assert.That(code, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.None));
+ Assert.That(seq, Is.GreaterThanOrEqualTo(0)); // some sequence number
+ Assert.That(body.Length, Is.EqualTo(2));
+ }
+
+ [Test]
+ public void GetSamples_ParsesSignedIntegers()
+ {
+ byte[] body = { 0x01, 0x00, 0xFF, 0xFF }; // 16-bit samples: 1 and -1
+
+ var result = NetSdrMessageHelper.GetSamples(16, body).ToArray();
+
+ Assert.That(result.Length, Is.EqualTo(2));
+ Assert.That(result[0], Is.EqualTo(1));
+ Assert.That(result[1], Is.EqualTo(-1));
+ }
+}
}
diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs
index b40fff79..613f4b94 100644
--- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs
+++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs
@@ -65,5 +65,69 @@ public void GetDataItemMessageTest()
}
//TODO: add more NetSdrMessageHelper tests
+ [Test]
+ public void GetSamples_ParsesSignedIntegers()
+ {
+ byte[] body = { 0x01, 0x00, 0xFF, 0xFF };
+
+ var result = NetSdrMessageHelper.GetSamples(16, body).ToArray();
+
+ Assert.That(result.Length, Is.EqualTo(2));
+ Assert.That(result[0], Is.EqualTo(1));
+ Assert.That(result[1], Is.EqualTo(-1));
+ }
+}
+
+ }
+
+ [Test]
+ public void TranslateMessage_DataItem_WorksCorrectly()
+ {
+ var raw = NetSdrMessageHelper.GetDataItemMessage(
+ NetSdrMessageHelper.MsgTypes.DataItem1,
+ new byte[] { 0x10, 0x20 });
+
+ bool ok = NetSdrMessageHelper.TranslateMessage(
+ raw, out var type, out var code, out var seq, out var body);
+
+ Assert.IsTrue(ok);
+ Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.DataItem1));
+ Assert.That(code, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.None));
+ Assert.That(seq, Is.GreaterThanOrEqualTo(0));
+ Assert.That(body.Length, Is.EqualTo(2));
+ }
+ [Test]
+ public void TranslateMessage_ControlItem_WorksCorrectly()
+ {
+ var raw = NetSdrMessageHelper.GetControlItemMessage(
+ NetSdrMessageHelper.MsgTypes.SetControlItem,
+ NetSdrMessageHelper.ControlItemCodes.RFFilter,
+ new byte[] { 0xAA });
+
+ bool ok = NetSdrMessageHelper.TranslateMessage(
+ raw, out var type, out var code, out var seq, out var body);
+
+ Assert.IsTrue(ok);
+ Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.SetControlItem));
+ Assert.That(code, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.RFFilter));
+ Assert.That(body[0], Is.EqualTo(0xAA));
}
-}
\ No newline at end of file
+using NetSdrClientApp.Messages;
+
+namespace NetSdrClientAppTests;
+
+public class NetSdrMessageHelperTests
+{
+ [Test]
+ public void GetControlItemMessage_CreatesValidMessage()
+ {
+ var msg = NetSdrMessageHelper.GetControlItemMessage(
+ NetSdrMessageHelper.MsgTypes.SetControlItem,
+ NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency,
+ new byte[] { 0x01, 0x02 });
+
+ Assert.That(msg.Length, Is.GreaterThan(4));
+ Assert.That(msg[2], Is.EqualTo(0x20));
+ }
+
+}
diff --git a/README.md b/README.md
index b3a90294..effa99f8 100644
--- a/README.md
+++ b/README.md
@@ -1,278 +1,18 @@
-# Лабораторні з реінжинірингу (8×)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
+Для початку необхідно встановити пакети coverlet.msbuild та Microsoft.NET.Test.Sdk, оскільки саме вони виконують функцію інструментів, що забезпечують збирання даних про покриття тестами. Зазначені пакети інтегруються у тестовий проєкт (NetSdrClientAppTests) та уможливлюють отримання показників тестового покриття в SonarCloud під час виконання лабораторної роботи. За їх відсутності отримання будь-яких числових значень щодо покриття тестами було б неможливим.
+Втсановленння пакетів:
+
+Підключення тестів
+
-Цей репозиторій використовується для курсу **реінжиніринг ПЗ**.
-Мета — провести комплексний реінжиніринг спадкового коду NetSdrClient, включаючи рефакторинг архітектури, покращення якості коду, впровадження сучасних практик розробки та автоматизацію процесів контролю якості через CI/CD пайплайни.
----
+Додаємо тести:
+
-## Структура 8 лабораторних
- Кожна робота — **через Pull Request або окремий commit**. Додати короткий опис: *що змінено / як перевірити* + звіт про хід виконання в Classroom.
+PR із новими тестами
+
+
-### Лаба 1 — Підключення SonarCloud і CI
-**Мета:** створити проект у SonarCloud, підключити GitHub Actions, запустити перший аналіз.
-
-**Необхідно:**
-- .NET 8 SDK
-- Публічний 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).
-
-**Здати:** посилання на PR чи commit, скрін Quality Gate, скрін бейджів у README.
-
----
-
-### Лаба 2 — Code Smells через PR + “gated merge”
-
-**Мета:** виправити **5–10** зауважень Sonar (bugs/smells) без зміни поведінки.
-
-**Кроки:**
-- Дрібними комітами виправити знайдені Sonar-проблеми у `NetSdrClientApp`.
-
-**Здати:** скріни змін метрик у Sonar.
-
----
-
-### Лаба 3 — Тести та покриття
-
-**Мета:** підняти покриття коду юніт-тестами в модулі.
-
-**Кроки:**
-- Підключити генерацію покриття:
- - `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 юніт-тестів
-
-**Здати:** PR із новими тестами, скрін Coverage у Sonar.
-
----
-
-### Лаба 4 — Дублікати через SonarCloud
-
-**Мета:** зменшити дублікати коду.
-
-**Кроки:**
-- Переглянути **Measures → Duplications** у Sonar і **Checks → SonarCloud** у PR.
-- Прибрати **1–2** найбільші дубльовані фрагменти (рефакторинг/винесення спільного коду).
-- Перезапустити CI, перевірити, що *Duplications on New Code* ≤ порога (типово 3%).
-
-**Здати:** PR з скрінами “до/після”.
-
----
-
-### Лаба 5 — Архітектурні правила (NetArchTest)
-
-**Мета:** дослідження архітектурних правила залежностей
-
-**Кроки:**
-- Додати кілька архітектурних правил залежностей (наприклад, `*.UI` не має залежати від `*.Infrastructure` напряму).
-- Переконатися, що порушення **ламає збірку** (червоний PR), а фікс — зеленить.
-
-**Здати:** PR із тестами правил, скрін невдалого прогону (до фіксу) і зеленого (після).
-
----
-
-### Лаба 6 — Безпечний рефакторинг під тести
-
-**Мета:** рефакторинг коду
-
-**Кроки:**
-- Додати проект з юніт тестами для `EchoServer`
-- Реалізувати необхідні зміни в `EchoServer` для покращення його придатності до тестування
-- Покрити код юніт-тестами
-
-**Здати:** PR + коротка таблиця метрик “до/після”.
-
----
-
-### Лаба 7 — Оновлення залежностей
-
-**Мета:**навчитись виявляти й виправляти уразливі залежності, користуватись інструментами GitHub Security (Dependency graph, Dependabot alerts/updates).
-
-**Кроки:**
-- `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+).
-
-**Здати:** PR з оновленням, скрін push-рану після мерджу, нотатки про ризики.
-
----
-
-### Лаба 8 — Чистий проєкт і gated build
-
-**Мета:** Домогтися зеленого 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 Gate
- - (Опц.) Require approvals (1–2)
- - (Опц.) Require branches to be up to date (щоб ребейзилися перед мерджем)
-- Після застосування останніх змін, перевірити що Pull Request не дозволяється залити, допоки Sonar не закінчить переврку
-
-
-**Здати:** скрін *Branches → main* з зеленим Gate
-
----
-
-## Норми здачі та оцінювання (єдині для всіх лаб)
-
-**Подання:** через **Pull Request** чи **commit**.
-**Опис:** що зроблено, як перевірити, ризики/зворотна сумісність.
-**Артефакти:** скріни/посилання на Sonar, логи CI.
-
----
-
-## Типові граблі → що робити
-
-- **“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”.