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×) -[![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) +Для початку необхідно встановити пакети coverlet.msbuild та Microsoft.NET.Test.Sdk, оскільки саме вони виконують функцію інструментів, що забезпечують збирання даних про покриття тестами. Зазначені пакети інтегруються у тестовий проєкт (NetSdrClientAppTests) та уможливлюють отримання показників тестового покриття в SonarCloud під час виконання лабораторної роботи. За їх відсутності отримання будь-яких числових значень щодо покриття тестами було б неможливим. +Втсановленння пакетів: + image +Підключення тестів + image -Цей репозиторій використовується для курсу **реінжиніринг ПЗ**. -Мета — провести комплексний реінжиніринг спадкового коду NetSdrClient, включаючи рефакторинг архітектури, покращення якості коду, впровадження сучасних практик розробки та автоматизацію процесів контролю якості через CI/CD пайплайни. ---- +Додаємо тести: + image -## Структура 8 лабораторних - Кожна робота — **через Pull Request або окремий commit**. Додати короткий опис: *що змінено / як перевірити* + звіт про хід виконання в Classroom. +PR із новими тестами + image + image -### Лаба 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 Gateimage - - (Опц.) Require approvals (1–2) - - (Опц.) Require branches to be up to date (щоб ребейзилися перед мерджем) -- Після застосування останніх змін, перевірити що Pull Request не дозволяється залити, допоки Sonar не закінчить переврку -image - -**Здати:** скрін *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”.