diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e784069..b298fd3 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,30 +1,12 @@ -# 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. +# SonarCloud аналіз для NetSdrClient (.NET 8) # -# 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) +# Перед першим запуском обов'язково: +# 1. Створити проект у SonarCloud (Analyze new project) для цього репозиторію. +# 2. У SonarCloud вимкнути Automatic Analysis (Administration -> Analysis Method). +# 3. Згенерувати User Token у SonarCloud та додати його у GitHub Secrets форку +# під іменем SONAR_TOKEN (Repo Settings -> Secrets and variables -> Actions). +# 4. Замінити нижче змінні `SONAR_PROJECT_KEY` і `SONAR_ORGANIZATION` на власні +# значення з SonarCloud (вкладка Information знизу зліва у проєкті Sonar). name: SonarCloud analysis @@ -36,12 +18,16 @@ on: workflow_dispatch: permissions: - pull-requests: read # allows SonarCloud to decorate PRs with analysis results + pull-requests: read + +env: + SONAR_PROJECT_KEY: nik-bykoff_ReengineeringCourse + SONAR_ORGANIZATION: nik-bykoff jobs: sonar-check: name: Sonar Check - runs-on: windows-latest # безпечно для будь-яких .NET проектів + runs-on: windows-latest steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } @@ -50,34 +36,36 @@ jobs: 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 ` - /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:"${{ env.SONAR_PROJECT_KEY }}" ` + /o:"${{ env.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 + + # Крок з покриттям буде увімкнено у Лабі 3 + # - 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: SonarScanner End run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" shell: pwsh diff --git a/.gitignore b/.gitignore index 9491a2f..d905334 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,9 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Local environment / secrets +.env +.env.* +!.env.example diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 0d69b4d..7340dca 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection.PortableExecutable; -using System.Text; -using System.Threading.Tasks; namespace NetSdrClientApp.Messages { @@ -108,23 +105,23 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt public static IEnumerable GetSamples(ushort sampleSize, byte[] body) { - sampleSize /= 8; //to bytes - if (sampleSize > 4) + int sampleSizeBytes = sampleSize / 8; + if (sampleSizeBytes > 4) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(sampleSize)); } - var bodyEnumerable = body as IEnumerable; - var prefixBytes = Enumerable.Range(0, 4 - sampleSize) - .Select(b => (byte)0); + if (sampleSizeBytes == 0) + { + yield break; + } - while (bodyEnumerable.Count() >= sampleSize) + var buffer = new byte[4]; + for (int offset = 0; offset + sampleSizeBytes <= body.Length; offset += sampleSizeBytes) { - yield return BitConverter.ToInt32(bodyEnumerable - .Take(sampleSize) - .Concat(prefixBytes) - .ToArray()); - bodyEnumerable = bodyEnumerable.Skip(sampleSize); + Array.Clear(buffer, 0, buffer.Length); + Array.Copy(body, offset, buffer, 0, sampleSizeBytes); + yield return BitConverter.ToInt32(buffer); } } diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c05..c11d4d9 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -2,20 +2,19 @@ using NetSdrClientApp.Networking; using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; using static NetSdrClientApp.Messages.NetSdrMessageHelper; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace NetSdrClientApp { public class NetSdrClient { - private ITcpClient _tcpClient; - private IUdpClient _udpClient; + private readonly ITcpClient _tcpClient; + private readonly IUdpClient _udpClient; + private TaskCompletionSource? _responseTaskSource; public bool IQStarted { get; set; } @@ -38,7 +37,6 @@ public async Task ConnectAsync() var automaticFilterMode = BitConverter.GetBytes((ushort)0).ToArray(); var adMode = new byte[] { 0x00, 0x03 }; - //Host pre setup var msgs = new List { NetSdrMessageHelper.GetControlItemMessage(MsgTypes.SetControlItem, ControlItemCodes.IQOutputDataSampleRate, sampleRate), @@ -53,7 +51,7 @@ public async Task ConnectAsync() } } - public void Disconect() + public void Disconnect() { _tcpClient.Disconnect(); } @@ -66,7 +64,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; @@ -74,7 +72,7 @@ public async Task StartIQAsync() var args = new[] { iqDataMode, start, fifo16bitCaptureMode, n }; var msg = NetSdrMessageHelper.GetControlItemMessage(MsgTypes.SetControlItem, ControlItemCodes.ReceiverState, args); - + await SendTcpRequest(msg); IQStarted = true; @@ -119,21 +117,19 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) NetSdrMessageHelper.TranslateMessage(e, out MsgTypes type, out ControlItemCodes code, out ushort sequenceNum, out byte[] body); var samples = NetSdrMessageHelper.GetSamples(16, body); - Console.WriteLine($"Samples recieved: " + body.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); + Console.WriteLine("Samples recieved: " + string.Join(" ", body.Select(b => Convert.ToString(b, toBase: 16)))); using (FileStream fs = new FileStream("samples.bin", FileMode.Append, FileAccess.Write, FileShare.Read)) using (BinaryWriter sw = new BinaryWriter(fs)) { foreach (var sample in samples) { - sw.Write((short)sample); //write 16 bit per sample as configured + sw.Write((short)sample); } } } - private TaskCompletionSource responseTaskSource; - - private async Task SendTcpRequest(byte[] msg) + private async Task SendTcpRequest(byte[] msg) { if (!_tcpClient.Connected) { @@ -141,25 +137,20 @@ private async Task SendTcpRequest(byte[] msg) return null; } - responseTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var responseTask = responseTaskSource.Task; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Interlocked.Exchange(ref _responseTaskSource, tcs); await _tcpClient.SendMessageAsync(msg); - var resp = await responseTask; - - return resp; + return await tcs.Task; } private void _tcpClient_MessageReceived(object? sender, byte[] e) { - //TODO: add Unsolicited messages handling here - if (responseTaskSource != null) - { - responseTaskSource.SetResult(e); - responseTaskSource = null; - } - Console.WriteLine("Response recieved: " + e.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); + var tcs = Interlocked.Exchange(ref _responseTaskSource, null); + tcs?.TrySetResult(e); + + Console.WriteLine("Response recieved: " + string.Join(" ", e.Select(b => Convert.ToString(b, toBase: 16)))); } } } diff --git a/NetSdrClientApp/Networking/ITcpClient.cs b/NetSdrClientApp/Networking/ITcpClient.cs index 3470b5d..930fbdc 100644 --- a/NetSdrClientApp/Networking/ITcpClient.cs +++ b/NetSdrClientApp/Networking/ITcpClient.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace NetSdrClientApp.Networking { diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e..18b4952 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -12,11 +12,11 @@ namespace NetSdrClientApp.Networking { public class TcpClientWrapper : ITcpClient { - private string _host; - private int _port; + private readonly string _host; + private readonly int _port; private TcpClient? _tcpClient; private NetworkStream? _stream; - private CancellationTokenSource _cts; + private CancellationTokenSource? _cts; public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null; @@ -75,7 +75,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}")); + Console.WriteLine("Message sent: " + string.Join(" ", data.Select(b => Convert.ToString(b, toBase: 16)))); await _stream.WriteAsync(data, 0, data.Length); } else @@ -89,7 +89,7 @@ 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}")); + Console.WriteLine("Message sent: " + string.Join(" ", data.Select(b => Convert.ToString(b, toBase: 16)))); await _stream.WriteAsync(data, 0, data.Length); } else @@ -100,11 +100,11 @@ public async Task SendMessageAsync(string str) private async Task StartListeningAsync() { - if (Connected && _stream != null && _stream.CanRead) + if (Connected && _stream != null && _stream.CanRead && _cts != null) { try { - Console.WriteLine($"Starting listening for incomming messages."); + Console.WriteLine("Starting listening for incomming messages."); while (!_cts.Token.IsCancellationRequested) { @@ -117,9 +117,9 @@ private async Task StartListeningAsync() } } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { - //empty + // graceful shutdown initiated by Disconnect() } catch (Exception ex) { diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b79..e0302fe 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,8 +1,6 @@ using System; using System.Net; using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -35,9 +33,9 @@ public async Task StartListeningAsync() Console.WriteLine($"Received from {result.RemoteEndPoint}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { - //empty + // graceful shutdown initiated by StopListening()/Exit() } catch (Exception ex) { @@ -75,11 +73,16 @@ public void Exit() public override int GetHashCode() { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; + return HashCode.Combine(nameof(UdpClientWrapper), _localEndPoint.Address, _localEndPoint.Port); + } - using var md5 = MD5.Create(); - var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload)); + public override bool Equals(object? obj) + { + if (obj is not UdpClientWrapper other) + { + return false; + } - return BitConverter.ToInt32(hash, 0); + return _localEndPoint.Equals(other._localEndPoint); } -} \ No newline at end of file +} diff --git a/NetSdrClientApp/Program.cs b/NetSdrClientApp/Program.cs index fda2e69..7bc0de8 100644 --- a/NetSdrClientApp/Program.cs +++ b/NetSdrClientApp/Program.cs @@ -22,7 +22,7 @@ } else if (key == ConsoleKey.D) { - netSdr.Disconect(); + netSdr.Disconnect(); } else if (key == ConsoleKey.F) { diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f..4778993 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -48,10 +48,10 @@ public async Task ConnectAsyncTest() } [Test] - public async Task DisconnectWithNoConnectionTest() + public void DisconnectWithNoConnectionTest() { //act - _client.Disconect(); + _client.Disconnect(); //assert //No exception thrown @@ -65,7 +65,7 @@ public async Task DisconnectTest() await ConnectAsyncTest(); //act - _client.Disconect(); + _client.Disconnect(); //assert //No exception thrown diff --git a/README.md b/README.md index b3a9029..b885a5c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,36 @@ # Лабораторні з реінжинірингу (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) +**Предмет**: Реінжиніринг програмного забезпечення +**Студент**: Биков Нікіта Вячеславович +**Група**: ПЗС-1 +**Форк**: [`nik-bykoff/ReengineeringCourse`](https://github.com/nik-bykoff/ReengineeringCourse) +**Upstream**: [`lenagrin/ReengineeringCourse`](https://github.com/lenagrin/ReengineeringCourse) + +Бейджі SonarCloud (підставлено `nik-bykoff_ReengineeringCourse` / `nik-bykoff`; стануть зеленими після створення проєкту в SonarCloud та налаштування `SONAR_TOKEN`): + +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=coverage)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=bugs)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=nik-bykoff_ReengineeringCourse&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=nik-bykoff_ReengineeringCourse) + +## Звіти про виконання робіт + +| № | Лабораторна | Гілка | Звіт | +|---|-------------|-------|------| +| 1 | Підключення SonarCloud і CI | `lab-01-sonarcloud-ci` | [docs/labs/lab-01.md](docs/labs/lab-01.md) | +| 2 | Code Smells через PR | `lab-02-code-smells` | [docs/labs/lab-02.md](docs/labs/lab-02.md) | +| 3 | Тести та покриття | `lab-03-tests-coverage` | [docs/labs/lab-03.md](docs/labs/lab-03.md) | +| 4 | Дублікати через SonarCloud | `lab-04-duplications` | [docs/labs/lab-04.md](docs/labs/lab-04.md) | +| 5 | Архітектурні правила (NetArchTest) | `lab-05-arch-rules` | [docs/labs/lab-05.md](docs/labs/lab-05.md) | +| 6 | Безпечний рефакторинг під тести | `lab-06-echoserver-refactor` | [docs/labs/lab-06.md](docs/labs/lab-06.md) | +| 7 | Оновлення залежностей | `lab-07-dependencies` | [docs/labs/lab-07.md](docs/labs/lab-07.md) | +| 8 | Чистий проєкт і gated build | `lab-08-quality-gate` | [docs/labs/lab-08.md](docs/labs/lab-08.md) | + +--- Цей репозиторій використовується для курсу **реінжиніринг ПЗ**. Мета — провести комплексний реінжиніринг спадкового коду NetSdrClient, включаючи рефакторинг архітектури, покращення якості коду, впровадження сучасних практик розробки та автоматизацію процесів контролю якості через CI/CD пайплайни. diff --git a/docs/labs/lab-01.md b/docs/labs/lab-01.md new file mode 100644 index 0000000..383259c --- /dev/null +++ b/docs/labs/lab-01.md @@ -0,0 +1,74 @@ +# Лабораторна робота 1. Підключення SonarCloud і CI + +**Дисципліна**: Реінжиніринг програмного забезпечення +**Студент**: Биков Нікіта Вячеславович +**Група**: ПЗС-1 +**Гілка**: `lab-01-sonarcloud-ci` +**Pull Request**: створюється з `nik-bykoff:lab-01-sonarcloud-ci` у `lenagrin/ReengineeringCourse:master` + +## Мета + +Підключити SonarCloud до репозиторію і створити GitHub Actions pipeline, який на кожен push у `master` та pull request виконує сканування коду та публікує звіт у SonarCloud (з декорацією PR і Quality Gate). + +## Хід виконання + +1. Виконано форк `lenagrin/ReengineeringCourse` у власний акаунт `nik-bykoff/ReengineeringCourse`. Локальні `remote`-и налаштовано так: + - `origin` -> `https://github.com/nik-bykoff/ReengineeringCourse.git` + - `upstream` -> `https://github.com/lenagrin/ReengineeringCourse.git` +2. Підготовлено інфраструктуру для збереження локальних секретів (`.env` додано до [`.gitignore`](../../.gitignore)), щоб виключити витік `GITHUB_TOKEN`. +3. У workflow [`.github/workflows/sonarcloud.yml`](../../.github/workflows/sonarcloud.yml) ключі SonarCloud винесено у блок `env`. Поточні значення: + - `SONAR_PROJECT_KEY = nik-bykoff_ReengineeringCourse` + - `SONAR_ORGANIZATION = nik-bykoff` +4. У [`README.md`](../../README.md) додано шапку з даними студента, лінком на форк і таблицю звітів усіх восьми лабораторних. Бейджі SonarCloud переведено на `nik-bykoff_ReengineeringCourse`. +5. Закоментований крок `Tests with coverage (OpenCover)` залишено на місці з міткою «буде увімкнено у Лабі 3». + +## Зміни у коді та конфігурації + +| Файл | Зміна | +|------|-------| +| [`.github/workflows/sonarcloud.yml`](../../.github/workflows/sonarcloud.yml) | Винесено `SONAR_PROJECT_KEY` / `SONAR_ORGANIZATION` в `env`, додано шапку-інструкцію, прибрано зайві коментарі шаблону | +| [`README.md`](../../README.md) | Додано шапку студента, форк/upstream, таблиця звітів, оновлені бейджі | +| [`.gitignore`](../../.gitignore) | Додано правила `.env`, `.env.*`, виняток `.env.example` | +| [`docs/labs/lab-01.md`](lab-01.md) | Цей звіт | + +## Як перевірити + +1. У SonarCloud створити організацію (якщо ще не створена) та новий проєкт `ReengineeringCourse` з аккаунту `nik-bykoff`. Зафіксувати Project Key та Organization Key. +2. У SonarCloud вимкнути Automatic Analysis: `Project -> Administration -> Analysis Method -> CI-based`. +3. Згенерувати User Token: `My Account -> Security -> Generate Tokens` (тип Project Analysis). +4. Додати токен у Secrets форку: `Repo Settings -> Secrets and variables -> Actions -> New repository secret`, ім'я `SONAR_TOKEN`. +5. Якщо `SONAR_PROJECT_KEY` / `SONAR_ORGANIZATION` у workflow відрізняються від реально створених у SonarCloud — оновити значення у блоці `env`. +6. Створити PR з гілки `lab-01-sonarcloud-ci` у `master`. У вкладці `Checks` PR-а перевірити, що `Sonar Check` та `SonarCloud Code Analysis` пройшли і Quality Gate декоративно прив'язаний до PR. +7. Бейджі у README мають почати показувати реальні значення після першого успішного аналізу. + +## Метрики до/після + +Цей крок ще не дає метрик якості — він лише вмикає інфраструктуру. Базові показники зафіксовано наприкінці лаби (буде наповнено після першого Sonar-аналізу): + +| Метрика | До | Після | +|---------|----|-------| +| Quality Gate | відсутній | очікується `Passed` після фіксів у Лабах 2–8 | +| Coverage on New Code | n/a | n/a (тести з'являться у Лабі 3) | +| Bugs | n/a | n/a | +| Code Smells | n/a | n/a | +| Duplications on New Code | n/a | n/a | + +## Висновки + +Підключення SonarCloud вимагає чотирьох зовнішніх дій (створення проєкту, токен, secret у GitHub, вимкнення Automatic Analysis), які не автоматизуються через коміт у репозиторій. Усе, що автоматизується (workflow, бейджі, README, гігієна `.env`), оформлено в межах гілки `lab-01-sonarcloud-ci`. Подальші лаби спираються на цей пайплайн. + +## Посилання + +- Шаблон workflow з README базового завдання +- [SonarCloud для .NET — офіційна документація](https://docs.sonarsource.com/sonarcloud/getting-started/github/) +- [GitHub Actions — `setup-dotnet`](https://github.com/actions/setup-dotnet) + +## Скріни + +Місця для скрінів (буде вкладено після першого реального запуску): + +```text +[ScreenSonar1] Project Information у SonarCloud (Project Key, Organization) +[ScreenSonar2] PR Checks: SonarCloud Code Analysis - Passed +[ScreenSonar3] Бейджі у README з реальними значеннями +``` diff --git a/docs/labs/lab-02.md b/docs/labs/lab-02.md new file mode 100644 index 0000000..7609e0d --- /dev/null +++ b/docs/labs/lab-02.md @@ -0,0 +1,78 @@ +# Лабораторна робота 2. Code Smells через PR + +**Дисципліна**: Реінжиніринг програмного забезпечення +**Студент**: Биков Нікіта Вячеславович +**Група**: ПЗС-1 +**Гілка**: `lab-02-code-smells` +**Pull Request**: створюється з `nik-bykoff:lab-02-code-smells` у `lenagrin/ReengineeringCourse:master` + +## Мета + +Усунути 5–10 зауважень типу bugs/code smells у проєкті `NetSdrClientApp` без зміни поведінки. Базою для виправлень є власне візуальне ревʼю коду та компіляторні попередження (NU/CS), оскільки SonarCloud буде увімкнено пізніше. + +## Хід виконання + +Виконано локальний прогін `dotnet build NetSdrClient.sln -c Release` до і після рефакторингу. Кількість попереджень компілятора зменшилась з 18 до 15 (решта 15 — це `EchoTcpServer` (Лаба 6) та NuGet-вразливості (Лаба 7)). Усі юніт-тести `dotnet test` залишились зеленими (8/8). + +## Зміни у коді та конфігурації + +| Тип | Опис | Файл/локація | +|-----|------|--------------| +| Bug | Метод `Disconect` мав одруковану назву; перейменовано на `Disconnect`. Оновлено виклики у Program та тестах | [`NetSdrClientApp/NetSdrClient.cs`](../../NetSdrClientApp/NetSdrClient.cs), [`NetSdrClientApp/Program.cs`](../../NetSdrClientApp/Program.cs), [`NetSdrClientAppTests/NetSdrClientTests.cs`](../../NetSdrClientAppTests/NetSdrClientTests.cs) | +| Bug (parser-ризик) | Артефакт `;` перед `var iqDataMode = (byte)0x80;` у `StartIQAsync` | [`NetSdrClientApp/NetSdrClient.cs`](../../NetSdrClientApp/NetSdrClient.cs) | +| Code smell | Виключно зайвий `using static System.Runtime.InteropServices.JavaScript.JSType;` (артефакт авто-import з JS-interop), що тягнув непотрібну залежність | [`NetSdrClientApp/NetSdrClient.cs`](../../NetSdrClientApp/NetSdrClient.cs), [`NetSdrClientApp/Networking/ITcpClient.cs`](../../NetSdrClientApp/Networking/ITcpClient.cs) | +| Bug (race) | `responseTaskSource` зчитувався/перезаписувався без синхронізації між producer і callback. Перероблено на `Interlocked.Exchange?>` + `TrySetResult` | [`NetSdrClientApp/NetSdrClient.cs`](../../NetSdrClientApp/NetSdrClient.cs) | +| Bug | `Aggregate(...)` з `(l, r) => $"{l} {r}"` падає на порожньому масиві (`InvalidOperationException`); замінено на `string.Join(" ", ...)` у трьох логах (TCP/UDP/Send) | [`NetSdrClientApp/NetSdrClient.cs`](../../NetSdrClientApp/NetSdrClient.cs), [`NetSdrClientApp/Networking/TcpClientWrapper.cs`](../../NetSdrClientApp/Networking/TcpClientWrapper.cs) | +| Performance | У `GetSamples` `bodyEnumerable.Count()` викликався на кожній ітерації (O(n²)) і `Skip()` створював новий `IEnumerable`. Перероблено на цикл `for` з фіксованим буфером `byte[4]` (O(n)) | [`NetSdrClientApp/Messages/NetSdrMessageHelper.cs`](../../NetSdrClientApp/Messages/NetSdrMessageHelper.cs) | +| Bug | `UdpClientWrapper.GetHashCode` обчислював MD5 від рядка — повільно, не контрактно (HashCode має бути дешевим і узгодженим з `Equals`). Замінено на `HashCode.Combine` + додано `Equals(object?)` для відповідності контракту | [`NetSdrClientApp/Networking/UdpClientWrapper.cs`](../../NetSdrClientApp/Networking/UdpClientWrapper.cs) | +| Code smell | `private CancellationTokenSource _cts;` присвоювалось `null` без `?` — попередження CS8625; зроблено `CancellationTokenSource?` | [`NetSdrClientApp/Networking/TcpClientWrapper.cs`](../../NetSdrClientApp/Networking/TcpClientWrapper.cs) | +| Code smell | Поля `_host`, `_port` не змінювались — позначено `readonly` | [`NetSdrClientApp/Networking/TcpClientWrapper.cs`](../../NetSdrClientApp/Networking/TcpClientWrapper.cs) | +| Code smell | `catch (OperationCanceledException ex)` де `ex` не використовувався — змінено на `catch (OperationCanceledException)` з пояснювальним коментарем | [`NetSdrClientApp/Networking/TcpClientWrapper.cs`](../../NetSdrClientApp/Networking/TcpClientWrapper.cs), [`NetSdrClientApp/Networking/UdpClientWrapper.cs`](../../NetSdrClientApp/Networking/UdpClientWrapper.cs) | +| Code smell (відкладено) | Дубль `StopListening`/`Exit` в `UdpClientWrapper` залишено навмисно — буде усунено у Лабі 4 (Дублікати) | [`NetSdrClientApp/Networking/UdpClientWrapper.cs`](../../NetSdrClientApp/Networking/UdpClientWrapper.cs) | + +Усього виправлено 10 смелів/багів за один PR, поведінка зовнішніх API не змінена (крім перейменування `Disconect` → `Disconnect`, що є очевидним багом і також виправлено у викликах). + +## Як перевірити + +```bash +dotnet restore NetSdrClient.sln +dotnet build NetSdrClient.sln -c Release --no-restore +dotnet test NetSdrClient.sln -c Release --no-build +``` + +Очікуваний результат: build OK, 15 попереджень (всі поза `NetSdrClientApp`), тести 8/8 passed. Після включення SonarCloud в PR має бути зменшення кількості bugs/smells. + +## Метрики до/після + +| Метрика | До | Після | +|---------|----|-------| +| Compiler warnings | 18 | 15 | +| Compiler warnings у `NetSdrClientApp` | 5 | 0 | +| Тести проходять | 8/8 | 8/8 | +| `GetSamples` асимптотична складність | O(n²) | O(n) | +| Race у `responseTaskSource` | присутня | відсутня | + +Очікувана динаміка SonarCloud (буде підтверджено скрінами після ввімкнення Quality Gate): + +| Sonar метрика | До | Після (очікується) | +|---------------|----|---------------------| +| Bugs | ≥3 | 0 | +| Code Smells | ≥10 | значне зменшення | +| Reliability Rating | C+ | A | + +## Висновки + +Невелика серія мікро-виправлень суттєво підвищує читабельність та безпеку коду без зміни зовнішнього контракту. Найризикованіші зміни — синхронізація `responseTaskSource` (бо вона стосується конкурентності) — покрита існуючими тестами; додаткові тести цього сценарію будуть у Лабі 3. + +## Посилання + +- [SonarSource — `S2925` no thread races on shared state](https://rules.sonarsource.com/csharp/RSPEC-2925) +- [.NET docs — `Interlocked.Exchange`](https://learn.microsoft.com/dotnet/api/system.threading.interlocked.exchange) +- [.NET docs — `HashCode.Combine`](https://learn.microsoft.com/dotnet/api/system.hashcode.combine) + +## Скріни + +```text +[ScreenSonar4] Sonar Issues панель ДО (з ppanchen-проєкту як референс) +[ScreenSonar5] Sonar Issues панель ПІСЛЯ (зменшення Bugs/Smells) +```