diff --git a/.github/assets/image.png b/.github/assets/image.png new file mode 100644 index 0000000..93caa5a Binary files /dev/null and b/.github/assets/image.png differ diff --git a/.github/assets/image1.png b/.github/assets/image1.png new file mode 100644 index 0000000..5a6b252 Binary files /dev/null and b/.github/assets/image1.png differ diff --git a/.github/assets/image10.png b/.github/assets/image10.png new file mode 100644 index 0000000..8adc899 Binary files /dev/null and b/.github/assets/image10.png differ diff --git a/.github/assets/image11.png b/.github/assets/image11.png new file mode 100644 index 0000000..dcd5c49 Binary files /dev/null and b/.github/assets/image11.png differ diff --git a/.github/assets/image13.png b/.github/assets/image13.png new file mode 100644 index 0000000..846b1bc Binary files /dev/null and b/.github/assets/image13.png differ diff --git a/.github/assets/image14.png b/.github/assets/image14.png new file mode 100644 index 0000000..ddca738 Binary files /dev/null and b/.github/assets/image14.png differ diff --git a/.github/assets/image15.png b/.github/assets/image15.png new file mode 100644 index 0000000..b163597 Binary files /dev/null and b/.github/assets/image15.png differ diff --git a/.github/assets/image16.png b/.github/assets/image16.png new file mode 100644 index 0000000..15e536b Binary files /dev/null and b/.github/assets/image16.png differ diff --git a/.github/assets/image2.png b/.github/assets/image2.png new file mode 100644 index 0000000..589b341 Binary files /dev/null and b/.github/assets/image2.png differ diff --git a/.github/assets/image3.png b/.github/assets/image3.png new file mode 100644 index 0000000..ec88736 Binary files /dev/null and b/.github/assets/image3.png differ diff --git a/.github/assets/image4.png b/.github/assets/image4.png new file mode 100644 index 0000000..31bfe56 Binary files /dev/null and b/.github/assets/image4.png differ diff --git a/.github/assets/image5.png b/.github/assets/image5.png new file mode 100644 index 0000000..ffac638 Binary files /dev/null and b/.github/assets/image5.png differ diff --git a/.github/assets/image6.png b/.github/assets/image6.png new file mode 100644 index 0000000..eb27ca1 Binary files /dev/null and b/.github/assets/image6.png differ diff --git a/.github/assets/image7.png b/.github/assets/image7.png new file mode 100644 index 0000000..ee3905d Binary files /dev/null and b/.github/assets/image7.png differ diff --git a/.github/assets/image8.png b/.github/assets/image8.png new file mode 100644 index 0000000..f52cc2c Binary files /dev/null and b/.github/assets/image8.png differ diff --git a/.github/assets/image9.png b/.github/assets/image9.png new file mode 100644 index 0000000..fdc0fff Binary files /dev/null and b/.github/assets/image9.png differ diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index b3715b0..ca43b51 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -19,7 +19,7 @@ public class EchoServer : IDisposable public int Port { get; private set; } public bool IsRunning { get; private set; } - // Added logger injection for testability (defaults to Console.WriteLine) + // Added logger injection for testability public EchoServer(int port, Action? logger = null) { Port = port; @@ -139,9 +139,8 @@ private void SendMessageCallback(object? state) { try { - Random rnd = new Random(); byte[] samples = new byte[1024]; - rnd.NextBytes(samples); + Random.Shared.NextBytes(samples); _sequence++; byte[] msg = new byte[] { 0x04, 0x84 } diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 7ccca7c..9562b05 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -57,6 +57,8 @@ public void Disconnect() if (Connected) { _cts?.Cancel(); + _cts?.Dispose(); // Properly disposes the CancellationTokenSource + _stream?.Close(); _tcpClient?.Close(); @@ -84,7 +86,7 @@ public async Task SendMessageAsync(byte[] data) } } - // Fixed Duplication: Delegates the actual sending logic to the byte[] overload + // Delegates the actual sending logic to the byte[] overload public async Task SendMessageAsync(string str) { var data = Encoding.UTF8.GetBytes(str); diff --git a/README.md b/README.md index e69de29..a620783 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,198 @@ +## Лабораторна робота 1 + +**Підключення SonarCloud і CI** + +Створено новий репозиторій на основі репозиторію курсу та підключено SonarCloud, вимкнено автоматичний аналіз. + +Посилання – https://github.com/m4xym/NetSDR_Lab.git + + +Перший PR (https://github.com/m4xym/NetSDR_Lab/pull/2) – не пройшов перевірку через недостатнє покриття коду та ризики безпеки, пов’язані з використанням ${{ secrets.SONAR_TOKEN }} безпосередньо в блоці run (sonarcloud.yml) + +![alt text](.github/assets/image.png) + +--- + +## Лабораторна робота 2 + +**Code Smells через PR + “gated merge”** + +Виявлено та виправлено 6 нових Code smells (Mantainability): + +![alt text](.github/assets/image1.png) + +- Make '_sampleWriter' 'readonly'. Fields that are only assigned in the constructor should be "readonly" +https://github.com/m4xym/NetSDR_Lab/commit/7a581ea95651b465146e14e95d0946a234ba1ee8 + +- Cannot convert null literal to non-nullable reference type. +https://github.com/m4xym/NetSDR_Lab/commit/edea573f091dcbd044110bfeaf5def14b119c7af + +- Non-nullable event 'UnsolicitedMessageReceived' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the event as nullable. +https://github.com/m4xym/NetSDR_Lab/commit/f1e6ef1a45b5b058cf224b84d20bdcd3000a73c0 + +- Remove the unused local variable 'code', 'sequenceNum', 'body'. +https://github.com/m4xym/NetSDR_Lab/commit/da7a11f287afc9bd4d9f5024ab24a8be5e3facc5 + +Результат: + +![alt text](.github/assets/image2.png) + +--- + +## Лабораторна робота 3 + +**Тести та покриття** + +Поточне покриття – 0%. + +![alt text](.github/assets/image3.png) + +Додано 6 unit тестів (PR https://github.com/m4xym/NetSDR_Lab/pull/5 ): + +- ConnectAsync_WhenAlreadyConnected_DoesNotConnectOrSend +Перевіряє, чи виклик методу connect для вже активного з'єднання безпечно достроково завершується. +- StopIQAsync_NoConnection_DoesNotSendRequest +Гарантує, що якщо застосунок намагається зупинити потік даних IQ під час відключення, метод коректно завершується без викидання помилок або спроб передачі команд через неактивний мережевий сокет. +- ChangeFrequencyAsync_ValidInputs_SendsCorrectMessage +Підтверджує основну функціональність методу налаштування частоти. Перевіряє, що передача конкретної частоти в герцах та індексу каналу успішно упаковує корисне навантаження та передає його до потоку TCP. +- TcpClient_UnsolicitedMessage_RaisesUnsolicitedMessageReceivedEvent +Тестує логіку маршрутизації TCP. Вводить імітовану фонову трансляцію з апаратного забезпечення та перевіряє, що подія - UnsolicitedMessageReceived успішно передається до програми, замість виконання завдання з очікуючим наказом. +- UdpClient_MessageReceived_CallsSampleWriterWithSamples +Перевіряє працездатність парсера даних шляхом введення в UDP-потік імітованого корисного навантаження у вигляді байтів IQ перевіряється, чи NetSdrMessageHelper успішно перетворює корисне навантаження та передає отримані зразки повністю в пам'яті до роз'єднаного інтерфейсу ISampleWriter. +- ChangeFrequencyAsync_WhenNotConnected_HandlesNullResponseGracefully +Перевіряє асинхронні механізми безпеки. Якщо команда запускається без активного з'єднання, базовий SendTcpRequest припиняється і повертає null. + +Покриття становить 81.38%: + +![alt text](.github/assets/image4.png) + +--- + +## Лабораторна робота 4 + +**Дублікати через SonarCloud** + +Поточний рівень дублікатів коду становить 9.8%: + +![alt text](.github/assets/image5.png) + + +Дублікати виявлено у файлах: + +- TcpClientWrapper.cs — дублювання (14 %) +Причина: 2 перевантаження для SendMessageAsync (одне приймає byte[], а інше – sting). +Виправлення: щоб перевантаження для string перетворює корисне навантаження, а потім негайно передає його до перевантаження для byte[]. + +- Дублювання в UdpClientWrapper.cs (28%) +Причина: Методи StopListening() та Exit() містять ідентичний блок try/catch, логіку скасування, закриття сокета та ведення журналу в консолі. Виправлення: Exit() викликає StopListening(). + +PR: https://github.com/m4xym/NetSDR_Lab/pull/8 + + +Знижено Дублювання коду до 0%: + +![alt text](.github/assets/image6.png) + +--- + +## Лабораторна робота 5 + +**Архітектурні правила (NetArchTest)** + +Додано 2 тести для перевірки архітектури + +- Ізоляція доменної логіки (Messages_ShouldNot_DependOn_Networking) – Розділення відповідальності: тест гарантує, що простір імен Messages (де відбувається парсинг байтів та бізнес-логіка) є повністю незалежним. Він перевіряє, що парсер нічого не знає про інфраструктуру (Networking). Це захищає ядро програми: ми можемо змінити механізм роботи сокетів, не торкаючись логіки обробки самих повідомлень. + +- Інверсія залежностей (ClientApp_Should_DependOn_Interfaces...) – Принцип інверсії залежностей: тест забороняє головному класу NetSdrClient напряму створювати або залежати від конкретних класів (наприклад, TcpClientWrapper). Він змушує клас працювати виключно через абстракції (ITcpClient). Завдяки цьому ми зберігаємо можливість легко підміняти реальні мережеві виклики на моки (Mocks) під час тестування. + +Для перевірки тестів додано тимчасовий демонстраційний метод BadArchitectureMethod у класі NetSdrMessageHelper, який навмисно створює жорстку прив'язку між незалежними модулями. Знаходячись у просторі імен Messages (який має відповідати лише за чисту логіку парсингу), він ініціалізує клас TcpClientWrapper з простору імен Networking. + +Його наявність у коді гарантовано порушує правило ізоляції доменної логіки. Завдання цього методу – викликати спрацьовування NetArchTest.Rules, "завалити" збірку (зробити PR червоним). + +PR: https://github.com/m4xym/NetSDR_Lab/pull/9 + +Провал тесту Messages_ShouldNot_DependOn_Networking ламає збірку: + +![alt text](.github/assets/image7.png) + +Виправлення (видалено BadArchitectureMethod): + +![alt text](.github/assets/image8.png) + +--- + +## Лабораторна робота 6 + +**Безпечний рефакторинг під тести** + +Поточне покриття EchoServer – 0%. + +![alt text](.github/assets/image9.png) + +PR: https://github.com/m4xym/NetSDR_Lab/pull/10 + +Рефакторинг EchoServer: +- Підтримка порту 0: якщо передати серверу значення 0, ОС автоматично призначить вільний тимчасовий порт. +Logger: функція Console.WriteLine замінюється вбудованим Action, що дозволяє тестам фіксувати або приховувати записи в журналі. +- Розмежування функціональних блоків: логіка виведення інформації винесена в клас ProcessStreamAsync, який приймає стандартний потік. + +Тести: +- StartAsync_AssignsDynamicPort_AndSetsIsRunningToTrue – Коректність ініціалізації сервера та правильну роботу з динамічними портами ОС: передає порт 0 під час створення сервера, що змушує ОС автоматично знайти та призначити будь-який вільний порт. Потім він перевіряє, що стан сервера IsRunning стає true, фактичний порт оновлюється на число більше за 0, а логер фіксує повідомлення про успішний старт. Це повністю усуває проблему "Address already in use", коли тести падають через зайнятий жорстко заданий порт (наприклад, 5000). +- Stop_SafeShutDownServer – Безпечне завершення роботи мережевого слухача: запускає сервер у фоновій задачі, дає йому час на ініціалізацію, а потім викликає метод Stop(). Він перевіряє, що внутрішній CancellationToken успішно скасовує очікування нових клієнтів, статус IsRunning змінюється на false, а процес зупинки не викидає фатальних винятків і фіксується у логах. +- Server_ReceivesAndEchoesDataCorrectly – здатність сервера приймати та повертати (Echo) байти без втрат: запускає сервер, після чого створює тестовий TcpClient та підключається до 127.0.0.1 на динамічно виділений порт. Тест відправляє текстовий рядок ("Hello, Echo Server!"), перетворений на масив байтів. Далі він вичитує відповідь із потоку та застосовує Assert для перевірки того, що отримана кількість байтів і сам текст збігаються до символу. Додатково перевіряється, чи логер зафіксував факт підключення клієнта та обсяг повернутих даних. + +Покриття нового коду становить 82.76%: + +![alt text](.github/assets/image10.png) + +--- + +## Лабораторна робота 7 + +**Оновлення залежностей** + +Список залежностей: + +![alt text](.github/assets/mage11.png) + +У налаштуваннях увімкнено Dependency graph + Dependabot alerts. + +- PR на оновлення coverlet.collector (https://github.com/m4xym/NetSDR_Lab/pull/13) + +У нових версіях Coverlet часто змінюється спосіб взаємодії з JIT-компілятором. Оновлення в цьому випадку може призвести до «фантомного» падіння покриття, коли показник покриття, що становив 80%, раптово впаде до 0%, оскільки інструментарій не зміг інтегруватися у структуру конкретного збірника. +Версії Coverlet часто прив’язані до конкретних версій MSBuild. Істотний перехід на нову версію може вимагати оновленої версії .NET SDK для збірки або на GitHub Actions runner. + +- PR на оновлення Microsoft.NET.Test.Sdk (https://github.com/m4xym/NetSDR_Lab/pull/14) + +Test SDK керує процесом виявлення тестів та інтеграцією з виконавцем. Значні зміни в цій частині можуть іноді спричиняти проблеми з виявленням тестів у Visual Studio або в певних адаптерах тестів CI/CD, якщо змінюється внутрішній протокол зв’язку виконавця. + +- PR на оновлення NUnit.Analyzers (https://github.com/m4xym/NetSDR_Lab/pull/16) + +- PR на оновлення NUnit (https://github.com/m4xym/NetSDR_Lab/pull/15) + +--- + +## Лабораторна робота 8 + +**Чистий проєкт і gated build** + +Статус проєкту: + +![alt text](.github/assets/image13.png) + +- Coverage - 89.6% +- Duplications - 0% +- 1 Security Hotspots + + +Виправлено Security Hotspot "Make sure that using this pseudorandom number generator is safe here." (PR https://github.com/m4xym/NetSDR_Lab/pull/21) + +Увімкнено правила Require a pull request before merging та Require status checks to pass (Sonar Check) + +![alt text](.github/assets/image14.png) + +![alt text](.github/assets/image15.png) + +Правило не дозволяє злиття до закінчення перевірки: + +![alt text](.github/assets/image16.png) \ No newline at end of file diff --git a/combined.md b/combined.md deleted file mode 100644 index 0636eb2..0000000 --- a/combined.md +++ /dev/null @@ -1,326 +0,0 @@ -### Project Structure -```text -/home/maksym/Projects/NetSDR_Lab -├── combined.md -├── EchoTcpServer -│   ├── EchoServer.csproj -│   └── Program.cs -├── EchoTcpServerTests -│   ├── EchoServerTests.cs -│   ├── EchoTcpServerTests.csproj -│   └── UdpTimedSenderTests.cs -├── NetSdrClientApp -│   ├── Messages -│   │   └── NetSdrMessageHelper.cs -│   ├── NetSdrClientApp.csproj -│   ├── NetSdrClient.cs -│   ├── Networking -│   │   ├── ITcpClient.cs -│   │   ├── IUdpClient.cs -│   │   ├── TcpClientWrapper.cs -│   │   └── UdpClientWrapper.cs -│   └── Program.cs -├── NetSdrClientAppTests -│   ├── ArchitectureTests.cs -│   ├── NetSdrClientAppTests.csproj -│   ├── NetSdrClientTests.cs -│   ├── NetSdrMessageHelperTests.cs -│   └── TestResults -│   ├── 6dd9ef29-ad39-4364-b891-02331057a83e -│   │   └── coverage.opencover.xml -│   └── coverage.xml -├── NetSdrClient.sln -└── README.md - -9 directories, 22 files -``` - -### Selected Files - -#### File: `/home/maksym/Projects/NetSDR_Lab/NetSdrClientApp/Networking/UdpClientWrapper.cs` -```cs -using System; -using System.Net; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -public class UdpClientWrapper : IUdpClient -{ - private readonly IPEndPoint _localEndPoint; - private CancellationTokenSource? _cts; - private UdpClient? _udpClient; - - public event EventHandler? MessageReceived; - - public UdpClientWrapper(int port) - { - _localEndPoint = new IPEndPoint(IPAddress.Any, port); - } - - public async Task StartListeningAsync() - { - _cts = new CancellationTokenSource(); - Console.WriteLine("Start listening for UDP messages..."); - - try - { - _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}"); - } - } - catch (OperationCanceledException ex) - { - //empty - } - catch (Exception ex) - { - Console.WriteLine($"Error receiving message: {ex.Message}"); - } - } - - public void StopListening() - { - try - { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) - { - Console.WriteLine($"Error while stopping: {ex.Message}"); - } - } - - // Fixed Duplication: Delegates directly to StopListening instead of copying the try/catch - public void Exit() - { - StopListening(); - } - - public override int GetHashCode() - { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; - - using var md5 = MD5.Create(); - var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload)); - - return BitConverter.ToInt32(hash, 0); - } -} -``` - -#### File: `/home/maksym/Projects/NetSDR_Lab/EchoTcpServer/Program.cs` -```cs -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Diagnostics.CodeAnalysis; - -namespace EchoTcpServer -{ - public class EchoServer : IDisposable - { - private TcpListener? _listener; - private CancellationTokenSource? _cancellationTokenSource; - private readonly Action _logger; - - public int Port { get; private set; } - public bool IsRunning { get; private set; } - - // Added logger injection for testability (defaults to Console.WriteLine) - public EchoServer(int port, Action? logger = null) - { - Port = port; - _logger = logger ?? Console.WriteLine; - } - - public async Task StartAsync() - { - _cancellationTokenSource = new CancellationTokenSource(); - _listener = new TcpListener(IPAddress.Any, Port); - _listener.Start(); - - // If Port 0 was passed, capture the actual OS-assigned port - Port = ((IPEndPoint)_listener.LocalEndpoint).Port; - IsRunning = true; - _logger($"Server started on port {Port}."); - - try - { - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - _logger("Client connected."); - - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - } - catch (ObjectDisposedException) - { - // Listener has been closed gracefully - } - catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) - { - // Listener stopped via cancellation token during AcceptTcpClientAsync - } - finally - { - IsRunning = false; - _logger("Server shutdown."); - } - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - // Enclose the client in a using block to guarantee disposal - using (client) - using (NetworkStream stream = client.GetStream()) - { - try - { - await ProcessStreamAsync(stream, token); - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - _logger($"Error: {ex.Message}"); - } - finally - { - _logger("Client disconnected."); - } - } - } - - // Extracted the core Echo logic to accept ANY stream (makes mocking/testing trivial) - public async Task ProcessStreamAsync(Stream stream, CancellationToken token) - { - 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); - _logger($"Echoed {bytesRead} bytes to the client."); - } - } - - public void Stop() - { - if (!IsRunning) return; - - _cancellationTokenSource?.Cancel(); - _listener?.Stop(); - _logger("Server stopped."); - } - - public void Dispose() - { - Stop(); - _cancellationTokenSource?.Dispose(); - } - } - - public class UdpTimedSender : IDisposable - { - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private Timer? _timer; - private ushort _sequence; - - public UdpTimedSender(string host, int port) - { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } - - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); - - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } - - private void SendMessageCallback(object? state) - { - try - { - Random rnd = new Random(); - byte[] samples = new byte[1024]; - rnd.NextBytes(samples); - _sequence++; - - byte[] msg = new byte[] { 0x04, 0x84 } - .Concat(BitConverter.GetBytes(_sequence)) - .Concat(samples) - .ToArray(); - - var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port}"); - } - catch (Exception ex) - { - Console.WriteLine($"Error sending message: {ex.Message}"); - } - } - - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } - - public void Dispose() - { - StopSending(); - _udpClient.Dispose(); - } - } - - [ExcludeFromCodeCoverage] - public class Program - { - public static async Task Main(string[] args) - { - using EchoServer server = new EchoServer(5000); - _ = Task.Run(() => server.StartAsync()); - - string host = "127.0.0.1"; - int port = 60000; - int intervalMilliseconds = 5000; - - 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) - { - // Wait until 'q' is pressed - } - - sender.StopSending(); - server.Stop(); - Console.WriteLine("Sender stopped."); - } - } -} -``` -