diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..446b951e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "nuget"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index e7840696..e538bdf2 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -56,8 +56,8 @@ jobs:
dotnet tool install --global dotnet-sonarscanner
echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH
dotnet sonarscanner begin `
- /k:"ppanchen_NetSdrClient" `
- /o:"ppanchen" `
+ /k:"YuraKozyyr_NetSdrClient" `
+ /o:"yurakozyr" `
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
/d:sonar.cs.opencover.reportsPaths="**/coverage.xml" `
/d:sonar.cpd.cs.minimumTokens=40 `
@@ -70,13 +70,14 @@ jobs:
run: dotnet restore NetSdrClient.sln
- name: Build
run: dotnet build NetSdrClient.sln -c Release --no-restore
- #- name: Tests with coverage (OpenCover)
- # run: |
- # dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build `
- # /p:CollectCoverage=true `
- # /p:CoverletOutput=TestResults/coverage.xml `
- # /p:CoverletOutputFormat=opencover
- # shell: pwsh
+ - name: Tests with coverage (OpenCover)
+ run: |
+ dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build `
+ /p:CollectCoverage=true `
+ /p:CoverletOutput=TestResults/coverage.xml `
+ /p:CoverletOutputFormat=opencover
+ dotnet test NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj -c Release --no-build
+ shell: pwsh
# 3) END: SonarScanner
- name: SonarScanner End
run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
diff --git a/NetSdrClient.sln b/NetSdrClient.sln
index 42431fb3..3e482753 100644
--- a/NetSdrClient.sln
+++ b/NetSdrClient.sln
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "Net
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientApp.ArchTests", "NetSdrClientApp.ArchTests\NetSdrClientApp.ArchTests.csproj", "{28EB108C-651C-4289-9900-3AAE9BB597F2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -27,6 +29,10 @@ Global
{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU
+ {28EB108C-651C-4289-9900-3AAE9BB597F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {28EB108C-651C-4289-9900-3AAE9BB597F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {28EB108C-651C-4289-9900-3AAE9BB597F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {28EB108C-651C-4289-9900-3AAE9BB597F2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/NetSdrClientApp.ArchTests/ArchitectureTests.cs b/NetSdrClientApp.ArchTests/ArchitectureTests.cs
new file mode 100644
index 00000000..99808861
--- /dev/null
+++ b/NetSdrClientApp.ArchTests/ArchitectureTests.cs
@@ -0,0 +1,87 @@
+using NetArchTest.Rules;
+using NetSdrClientApp;
+using NetSdrClientApp.Messages;
+using NetSdrClientApp.Networking;
+using NUnit.Framework;
+
+namespace NetSdrClientApp.ArchTests;
+
+[TestFixture]
+public class ArchitectureTests
+{
+ ///
+ /// Головна збірка клієнта не повинна залежати від тестового Echo-сервера.
+ /// (Echo-сервер – лише допоміжний інструмент для тестування)
+ ///
+ [Test]
+ public void NetSdrClientApp_Should_Not_Depend_On_EchoTcpServer()
+ {
+ var result = Types
+ .InAssembly(typeof(NetSdrClient).Assembly)
+ .ShouldNot()
+ .HaveDependencyOn("EchoServer") // назва збірки EchoTcpServer
+ .GetResult();
+
+ Assert.That(result.IsSuccessful, Is.True,
+ "NetSdrClientApp не повинен мати залежність від EchoTcpServer (EchoServer).");
+ }
+
+ ///
+ /// Повідомлення (Messages) не мають тягнути за собою залежності від мережевого шару.
+ ///
+ [Test]
+ public void Messages_Should_Not_Depend_On_Networking()
+ {
+ var result = Types
+ .InAssembly(typeof(NetSdrMessageHelper).Assembly)
+ .That()
+ .ResideInNamespace("NetSdrClientApp.Messages")
+ .ShouldNot()
+ .HaveDependencyOn("NetSdrClientApp.Networking")
+ .GetResult();
+
+ Assert.That(result.IsSuccessful, Is.True,
+ "NetSdrClientApp.Messages не повинен залежати від NetSdrClientApp.Networking.");
+ }
+
+ ///
+ /// Навпаки: мережевий шар не має залежати від Messages,
+ /// щоб уникнути циклічних залежностей.
+ ///
+ [Test]
+ public void Networking_Should_Not_Depend_On_Messages()
+ {
+ var result = Types
+ .InAssembly(typeof(TcpClientWrapper).Assembly)
+ .That()
+ .ResideInNamespace("NetSdrClientApp.Networking")
+ .ShouldNot()
+ .HaveDependencyOn("NetSdrClientApp.Messages")
+ .GetResult();
+
+ Assert.That(result.IsSuccessful, Is.True,
+ "NetSdrClientApp.Networking не повинен залежати від NetSdrClientApp.Messages.");
+ }
+
+ ///
+ /// Усі типи в просторі імен Networking мають бути *Wrapper*-ами.
+ ///
+ [Test]
+ public void Networking_Types_Should_Have_Names_Ending_With_Wrapper()
+ {
+ var result = Types
+ .InAssembly(typeof(TcpClientWrapper).Assembly)
+ .That()
+ .ResideInNamespace("NetSdrClientApp.Networking")
+ .And()
+ .AreClasses() // 🔹 важливо: тільки класи, без інтерфейсів
+ .Should()
+ .HaveNameEndingWith("Wrapper")
+ .GetResult();
+
+ Assert.That(result.IsSuccessful, Is.True,
+ "У NetSdrClientApp.Networking мають бути лише класи, назва яких закінчується на 'Wrapper'.");
+ }
+
+}
+
diff --git a/NetSdrClientApp.ArchTests/GlobalUsings.cs b/NetSdrClientApp.ArchTests/GlobalUsings.cs
new file mode 100644
index 00000000..cefced49
--- /dev/null
+++ b/NetSdrClientApp.ArchTests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
\ No newline at end of file
diff --git a/NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj b/NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj
new file mode 100644
index 00000000..fa9e0620
--- /dev/null
+++ b/NetSdrClientApp.ArchTests/NetSdrClientApp.ArchTests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs
index b0a7c058..29ac70d1 100644
--- a/NetSdrClientApp/NetSdrClient.cs
+++ b/NetSdrClientApp/NetSdrClient.cs
@@ -14,8 +14,8 @@ namespace NetSdrClientApp
{
public class NetSdrClient
{
- private ITcpClient _tcpClient;
- private IUdpClient _udpClient;
+ private readonly ITcpClient _tcpClient;
+ private readonly IUdpClient _udpClient;
public bool IQStarted { get; set; }
@@ -66,7 +66,7 @@ public async Task StartIQAsync()
return;
}
-; var iqDataMode = (byte)0x80;
+ var iqDataMode = (byte)0x80;
var start = (byte)0x02;
var fifo16bitCaptureMode = (byte)0x01;
var n = (byte)1;
@@ -114,9 +114,14 @@ public async Task ChangeFrequencyAsync(long hz, int channel)
await SendTcpRequest(msg);
}
- private void _udpClient_MessageReceived(object? sender, byte[] e)
+ private static void _udpClient_MessageReceived(object? sender, byte[] e)
{
- NetSdrMessageHelper.TranslateMessage(e, out MsgTypes type, out ControlItemCodes code, out ushort sequenceNum, out byte[] body);
+ NetSdrMessageHelper.TranslateMessage(
+ e,
+ out _, // MsgTypes type не потрібен
+ out _, // ControlItemCodes code не потрібен
+ out _, // ushort sequenceNum не потрібен
+ out var body); // тільки body використовуємо
var samples = NetSdrMessageHelper.GetSamples(16, body);
Console.WriteLine($"Samples recieved: " + body.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}"));
diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj
index 2ac91006..cb3674d8 100644
--- a/NetSdrClientApp/NetSdrClientApp.csproj
+++ b/NetSdrClientApp/NetSdrClientApp.csproj
@@ -7,8 +7,8 @@
enable
-
-
+
+
diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs
index 1f37e2e5..8ef0df0e 100644
--- a/NetSdrClientApp/Networking/TcpClientWrapper.cs
+++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs
@@ -1,8 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading;
@@ -10,13 +7,15 @@
namespace NetSdrClientApp.Networking
{
- public class TcpClientWrapper : ITcpClient
+ public class TcpClientWrapper : ITcpClient, IDisposable
{
- 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;
+ private bool _disposed;
public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null;
@@ -30,52 +29,98 @@ public TcpClientWrapper(string host, int port)
public void Connect()
{
+ ThrowIfDisposed();
+
if (Connected)
{
Console.WriteLine($"Already connected to {_host}:{_port}");
return;
}
+ // прибираємо попередню CTS (якщо була)
+ _cts?.Cancel();
+ _cts?.Dispose();
+ _cts = new CancellationTokenSource();
+
_tcpClient = new TcpClient();
try
{
- _cts = new CancellationTokenSource();
_tcpClient.Connect(_host, _port);
_stream = _tcpClient.GetStream();
Console.WriteLine($"Connected to {_host}:{_port}");
- _ = StartListeningAsync();
+
+ // запускаємо слухача з токеном цієї CTS
+ _ = StartListeningAsync(_cts.Token);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to connect: {ex.Message}");
+ // при фейлі не тримаємо зайві ресурси
+ _stream?.Dispose();
+ _tcpClient?.Dispose();
+ _cts?.Dispose();
+
+ _stream = null;
+ _tcpClient = null;
+ _cts = null;
}
}
public void Disconnect()
+ {
+ // не даємо викликати метод після Dispose()
+ ThrowIfDisposed();
+
+ if (!Connected)
+ {
+ Console.WriteLine("No active connection to disconnect.");
+ return;
+ }
+
+ try
+ {
+ _cts?.Cancel();
+ _stream?.Close();
+ _tcpClient?.Close();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error while disconnecting: {ex.Message}");
+ }
+ finally
+ {
+ _stream?.Dispose();
+ _tcpClient?.Dispose();
+ _cts?.Dispose();
+
+ _stream = null;
+ _tcpClient = null;
+ _cts = null;
+ }
+ }
+
+ public Task SendMessageAsync(byte[] data)
{
- if (Connected)
- {
- _cts?.Cancel();
- _stream?.Close();
- _tcpClient?.Close();
+ return SendMessageInternalAsync(data);
+ }
- _cts = null;
- _tcpClient = null;
- _stream = null;
- Console.WriteLine("Disconnected.");
- }
- else
- {
- Console.WriteLine("No active connection to disconnect.");
- }
+ public Task SendMessageAsync(string str)
+ {
+ var data = Encoding.UTF8.GetBytes(str);
+ return SendMessageInternalAsync(data);
}
- public async Task SendMessageAsync(byte[] data)
+ private async Task SendMessageInternalAsync(byte[] data)
{
+ ThrowIfDisposed();
+
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: " +
+ data.Select(b => Convert.ToString(b, toBase: 16))
+ .Aggregate((l, r) => $"{l} {r}"));
+
await _stream.WriteAsync(data, 0, data.Length);
}
else
@@ -84,57 +129,69 @@ public async Task SendMessageAsync(byte[] data)
}
}
- public async Task SendMessageAsync(string str)
+ private async Task StartListeningAsync(CancellationToken token)
{
- var data = Encoding.UTF8.GetBytes(str);
- if (Connected && _stream != null && _stream.CanWrite)
- {
- Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}"));
- await _stream.WriteAsync(data, 0, data.Length);
- }
- else
+ if (_stream == null || !_stream.CanRead)
{
throw new InvalidOperationException("Not connected to a server.");
}
- }
- private async Task StartListeningAsync()
- {
- if (Connected && _stream != null && _stream.CanRead)
+ try
{
- try
- {
- Console.WriteLine($"Starting listening for incomming messages.");
+ Console.WriteLine("Starting listening for incoming messages.");
- while (!_cts.Token.IsCancellationRequested)
- {
- byte[] buffer = new byte[8194];
+ var buffer = new byte[8194];
- int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token);
- if (bytesRead > 0)
- {
- MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray());
- }
- }
- }
- catch (OperationCanceledException ex)
- {
- //empty
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Error in listening loop: {ex.Message}");
- }
- finally
+ while (!token.IsCancellationRequested)
{
- Console.WriteLine("Listener stopped.");
+ var bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, token);
+ if (bytesRead <= 0)
+ {
+ // зʼєднання закрите
+ break;
+ }
+
+ var data = buffer.AsSpan(0, bytesRead).ToArray();
+ MessageReceived?.Invoke(this, data);
}
}
- else
+ catch (OperationCanceledException)
{
- throw new InvalidOperationException("Not connected to a server.");
+ // очікувано при відключенні
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error in listening loop: {ex.Message}");
+ }
+ finally
+ {
+ Console.WriteLine("Listener stopped.");
}
}
- }
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(TcpClientWrapper));
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _disposed = true;
+
+ _cts?.Cancel();
+
+ _stream?.Dispose();
+ _tcpClient?.Dispose();
+ _cts?.Dispose();
+
+ _stream = null;
+ _tcpClient = null;
+ _cts = null;
+ }
+ }
}
+
diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs
index 31e0b798..613ad58d 100644
--- a/NetSdrClientApp/Networking/UdpClientWrapper.cs
+++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs
@@ -1,85 +1,145 @@
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
+namespace NetSdrClientApp.Networking
{
- private readonly IPEndPoint _localEndPoint;
- private CancellationTokenSource? _cts;
- private UdpClient? _udpClient;
-
- public event EventHandler? MessageReceived;
-
- public UdpClientWrapper(int port)
+ public sealed 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;
+
+ public UdpClientWrapper(int port)
+ {
+ _localEndPoint = new IPEndPoint(IPAddress.Any, port);
+ }
- try
+ public async Task StartListeningAsync()
{
- _udpClient = new UdpClient(_localEndPoint);
- while (!_cts.Token.IsCancellationRequested)
+ ThrowIfDisposed();
+
+ if (_cts is not null)
+ {
+ Console.WriteLine("UDP listener is already running.");
+ return;
+ }
+
+ _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);
- Console.WriteLine($"Received from {result.RemoteEndPoint}");
+ while (!_cts.IsCancellationRequested)
+ {
+ UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token);
+ MessageReceived?.Invoke(this, result.Buffer);
+ Console.WriteLine($"Received from {result.RemoteEndPoint}");
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // очікувано при зупинці
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error receiving message: {ex.Message}");
+ }
+ finally
+ {
+ // гарантовано звільняємо ресурси
+ DisposeUdpResources();
}
}
- catch (OperationCanceledException ex)
+
+ public void StopListening()
{
- //empty
+ StopInternal("StopListening");
}
- catch (Exception ex)
+
+ public void Exit()
{
- Console.WriteLine($"Error receiving message: {ex.Message}");
+ StopInternal("Exit");
}
- }
- public void StopListening()
- {
- try
+ private void StopInternal(string reason)
{
- _cts?.Cancel();
- _udpClient?.Close();
- Console.WriteLine("Stopped listening for UDP messages.");
+ ThrowIfDisposed();
+
+ try
+ {
+ if (_cts is null && _udpClient is null)
+ {
+ Console.WriteLine($"UDP listener already stopped ({reason}).");
+ return;
+ }
+
+ _cts?.Cancel();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error while cancelling token: {ex.Message}");
+ }
+ finally
+ {
+ DisposeUdpResources();
+ Console.WriteLine("Stopped listening for UDP messages.");
+ }
}
- catch (Exception ex)
+
+ private void DisposeUdpResources()
{
- Console.WriteLine($"Error while stopping: {ex.Message}");
+ _cts?.Dispose();
+ _udpClient?.Dispose();
+
+ _cts = null;
+ _udpClient = null;
}
- }
- public void Exit()
- {
- try
+ // ---- Рівність та хеш-код без MD5 ----
+
+ public override int GetHashCode()
{
- _cts?.Cancel();
- _udpClient?.Close();
- Console.WriteLine("Stopped listening for UDP messages.");
+ // Без криптографії – просто комбінуємо адресу та порт.
+ return HashCode.Combine(_localEndPoint.Address, _localEndPoint.Port);
}
- catch (Exception ex)
+
+ public override bool Equals(object? obj)
{
- Console.WriteLine($"Error while stopping: {ex.Message}");
+ if (ReferenceEquals(this, obj))
+ return true;
+
+ if (obj is not UdpClientWrapper other)
+ return false;
+
+ return Equals(_localEndPoint, other._localEndPoint);
}
- }
- public override int GetHashCode()
- {
- var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}";
+ // ---- Dispose pattern ----
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(UdpClientWrapper));
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
- using var md5 = MD5.Create();
- var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload));
+ _disposed = true;
- return BitConverter.ToInt32(hash, 0);
+ _cts?.Cancel();
+ DisposeUdpResources();
+ }
}
-}
\ No newline at end of file
+}
+
diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj
index 3cbc46af..0a44ab1d 100644
--- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj
+++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj
@@ -11,7 +11,11 @@
-
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs
index ad00c4f8..5b9bfb85 100644
--- a/NetSdrClientAppTests/NetSdrClientTests.cs
+++ b/NetSdrClientAppTests/NetSdrClientTests.cs
@@ -1,6 +1,11 @@
-using Moq;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Moq;
using NetSdrClientApp;
+using NetSdrClientApp.Messages;
using NetSdrClientApp.Networking;
+using NUnit.Framework;
namespace NetSdrClientAppTests;
@@ -26,10 +31,11 @@ public void Setup()
_tcpMock.Setup(tcp => tcp.Connected).Returns(false);
});
- _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())).Callback((bytes) =>
- {
- _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes);
- });
+ _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny()))
+ .Callback((bytes) =>
+ {
+ _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes);
+ });
_updMock = new Mock();
@@ -39,10 +45,8 @@ public void Setup()
[Test]
public async Task ConnectAsyncTest()
{
- //act
await _client.ConnectAsync();
- //assert
_tcpMock.Verify(tcp => tcp.Connect(), Times.Once);
_tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(3));
}
@@ -50,37 +54,26 @@ public async Task ConnectAsyncTest()
[Test]
public async Task DisconnectWithNoConnectionTest()
{
- //act
_client.Disconect();
- //assert
- //No exception thrown
_tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once);
}
[Test]
public async Task DisconnectTest()
{
- //Arrange
await ConnectAsyncTest();
- //act
_client.Disconect();
- //assert
- //No exception thrown
_tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once);
}
[Test]
public async Task StartIQNoConnectionTest()
{
-
- //act
await _client.StartIQAsync();
- //assert
- //No exception thrown
_tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never);
_tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce);
}
@@ -88,14 +81,10 @@ public async Task StartIQNoConnectionTest()
[Test]
public async Task StartIQTest()
{
- //Arrange
await ConnectAsyncTest();
- //act
await _client.StartIQAsync();
- //assert
- //No exception thrown
_updMock.Verify(udp => udp.StartListeningAsync(), Times.Once);
Assert.That(_client.IQStarted, Is.True);
}
@@ -103,17 +92,76 @@ public async Task StartIQTest()
[Test]
public async Task StopIQTest()
{
- //Arrange
await ConnectAsyncTest();
- //act
await _client.StopIQAsync();
- //assert
- //No exception thrown
_updMock.Verify(tcp => tcp.StopListening(), Times.Once);
Assert.That(_client.IQStarted, Is.False);
}
- //TODO: cover the rest of the NetSdrClient code here
+ [Test]
+ public async Task StopIQAsync_WhenNotConnected_DoesNotThrow()
+ {
+ var tcpMock = new Mock();
+ tcpMock.SetupGet(t => t.Connected).Returns(false);
+
+ var udpMock = new Mock();
+
+ var client = new NetSdrClient(tcpMock.Object, udpMock.Object);
+
+ Assert.DoesNotThrowAsync(async () => await client.StopIQAsync());
+ }
+
+ [Test]
+ public async Task ChangeFrequencyAsync_WhenNotConnected_DoesNotThrowAndUsesSendTcpRequestBranch()
+ {
+ var tcpMock = new Mock();
+ tcpMock.SetupGet(t => t.Connected).Returns(false);
+
+ var udpMock = new Mock();
+
+ var client = new NetSdrClient(tcpMock.Object, udpMock.Object);
+
+ Assert.DoesNotThrowAsync(async () => await client.ChangeFrequencyAsync(20_000_000, 1));
+ }
+
+ [Test]
+ public void UdpClient_MessageReceived_WritesSamplesToFile()
+ {
+ var tcpMock = new Mock();
+ tcpMock.SetupGet(t => t.Connected).Returns(true);
+
+ var udpMock = new Mock();
+
+ EventHandler? handler = null;
+
+ udpMock.SetupAdd(u => u.MessageReceived += It.IsAny>())
+ .Callback>(h => handler += h);
+
+ udpMock.SetupRemove(u => u.MessageReceived -= It.IsAny>())
+ .Callback>(h => handler -= h);
+
+ var client = new NetSdrClient(tcpMock.Object, udpMock.Object);
+
+ const string fileName = "samples.bin";
+ if (File.Exists(fileName))
+ {
+ File.Delete(fileName);
+ }
+
+ var body = new byte[] { 0x00, 0x01, 0x00, 0x02 }; // два 16-бітні семпли
+ var msg = NetSdrMessageHelper.GetDataItemMessage(NetSdrMessageHelper.MsgTypes.DataItem0, body);
+
+ // емулюємо прихід UDP-повідомлення
+ handler?.Invoke(this, msg);
+
+ Assert.That(File.Exists(fileName), Is.True, "Файл samples.bin мав бути створений");
+
+ var length = new FileInfo(fileName).Length;
+ Assert.That(length, Is.GreaterThan(0), "Файл samples.bin має містити дані");
+
+ File.Delete(fileName);
+ }
}
+
diff --git a/NetSdrClientAppTests/TcpClientWrapperTests.cs b/NetSdrClientAppTests/TcpClientWrapperTests.cs
new file mode 100644
index 00000000..93eb2d4a
--- /dev/null
+++ b/NetSdrClientAppTests/TcpClientWrapperTests.cs
@@ -0,0 +1,344 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using NetSdrClientApp.Networking;
+using NUnit.Framework;
+
+namespace NetSdrClientAppTests.Networking
+{
+ [TestFixture]
+ public class TcpClientWrapperTests
+ {
+ private const string Host = "127.0.0.1";
+
+ // ----------------- helpers -----------------
+
+ private static void SetPrivateField(TcpClientWrapper wrapper, string fieldName, T? value)
+ {
+ var field = typeof(TcpClientWrapper).GetField(fieldName,
+ BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.NotNull(field, $"Field '{fieldName}' was not found via reflection.");
+ field!.SetValue(wrapper, value);
+ }
+
+ private static Task InvokeStartListeningAsync(TcpClientWrapper wrapper, CancellationToken token)
+ {
+ var method = typeof(TcpClientWrapper).GetMethod("StartListeningAsync",
+ BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.NotNull(method, "StartListeningAsync not found via reflection.");
+
+ var taskObj = method!.Invoke(wrapper, new object[] { token });
+ return (Task)taskObj!;
+ }
+
+ ///
+ /// Стрім, який кидає в Close(), але мовчить у Dispose().
+ /// Використовується, щоб зайти в catch у Disconnect.
+ ///
+ private sealed class ThrowOnCloseNetworkStream : NetworkStream
+ {
+ public ThrowOnCloseNetworkStream(Socket socket)
+ : base(socket, FileAccess.ReadWrite, ownsSocket: false)
+ {
+ }
+
+ public override void Close()
+ {
+ throw new InvalidOperationException("Close boom");
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ // спеціально нічого не робимо, щоб finally у Disconnect не впав
+ }
+ }
+
+ ///
+ /// Стрім, який кидає в ReadAsync, але мовчить у Dispose().
+ /// Використовується, щоб зайти в catch (Exception ex) у StartListeningAsync.
+ ///
+ private sealed class ThrowOnReadNetworkStream : NetworkStream
+ {
+ public ThrowOnReadNetworkStream(Socket socket)
+ : base(socket, FileAccess.ReadWrite, ownsSocket: false)
+ {
+ }
+
+ public override bool CanRead => true;
+
+ public override Task ReadAsync(
+ byte[] buffer, int offset, int size, CancellationToken cancellationToken)
+ {
+ throw new InvalidOperationException("ReadAsync boom");
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ // глушимо, щоб finally у StartListeningAsync завершився без винятку
+ }
+ }
+
+ // ----------------- базові сценарії Connect / Send / Disconnect -----------------
+
+ [Test]
+ public void Connect_WhenServerIsUnavailable_DoesNotThrow_AndStaysDisconnected()
+ {
+ var wrapper = new TcpClientWrapper(Host, 1);
+
+ Assert.DoesNotThrow(() => wrapper.Connect());
+ Assert.That(wrapper.Connected, Is.False);
+ }
+
+ [Test]
+ public async Task Connect_And_SendMessage_WorksWithLocalListener()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ var wrapper = new TcpClientWrapper(Host, port);
+
+ var acceptTask = listener.AcceptTcpClientAsync();
+
+ Assert.DoesNotThrow(() => wrapper.Connect());
+ Assert.True(wrapper.Connected, "Wrapper should be connected after Connect().");
+
+ const string message = "hello";
+ await wrapper.SendMessageAsync(message);
+
+ using var serverClient = await acceptTask;
+ using var stream = serverClient.GetStream();
+
+ var buffer = new byte[message.Length];
+ var read = await stream.ReadAsync(buffer, 0, buffer.Length);
+ var received = Encoding.UTF8.GetString(buffer, 0, read);
+
+ Assert.That(received, Is.EqualTo(message));
+
+ wrapper.Disconnect();
+ listener.Stop();
+ }
+
+ [Test]
+ public void Connect_WhenAlreadyConnected_DoesNotThrow_AndStaysConnected()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ var wrapper = new TcpClientWrapper(Host, port);
+ var acceptTask = listener.AcceptTcpClientAsync();
+
+ wrapper.Connect();
+ using var _ = acceptTask.Result; // лише для встановлення конекта
+
+ Assert.IsTrue(wrapper.Connected, "First connect failed.");
+
+ Assert.DoesNotThrow(() => wrapper.Connect());
+ Assert.IsTrue(wrapper.Connected, "After second Connect() we still must be connected.");
+
+ wrapper.Disconnect();
+ listener.Stop();
+ }
+
+ [Test]
+ public void Disconnect_WhenNotConnected_DoesNotThrow()
+ {
+ var wrapper = new TcpClientWrapper("localhost", 5555);
+
+ Assert.DoesNotThrow(() => wrapper.Disconnect());
+ Assert.False(wrapper.Connected);
+ }
+
+ [Test]
+ public async Task Dispose_WhenConnected_ReleasesResourcesAndDisconnects()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ var acceptTask = listener.AcceptTcpClientAsync();
+ var wrapper = new TcpClientWrapper(Host, port);
+
+ wrapper.Connect();
+ using var _ = await acceptTask;
+
+ Assert.That(wrapper.Connected, Is.True);
+
+ wrapper.Dispose();
+
+ Assert.That(wrapper.Connected, Is.False);
+
+ listener.Stop();
+ }
+
+ [Test]
+ public void Dispose_CalledTwice_DoesNotThrow()
+ {
+ var wrapper = new TcpClientWrapper(Host, 12345);
+
+ wrapper.Dispose();
+ Assert.DoesNotThrow(() => wrapper.Dispose());
+ }
+
+ [Test]
+ public void SendMessageAsync_WhenNotConnected_ThrowsInvalidOperationException()
+ {
+ var wrapper = new TcpClientWrapper(Host, 65000);
+
+ var ex = Assert.ThrowsAsync(
+ async () => await wrapper.SendMessageAsync("hello"));
+
+ Assert.That(ex!.Message, Is.EqualTo("Not connected to a server."));
+ }
+
+ // ----------------- ThrowIfDisposed -----------------
+
+ [Test]
+ public void Connect_AfterDispose_ThrowsObjectDisposedException()
+ {
+ var wrapper = new TcpClientWrapper(Host, 12345);
+ wrapper.Dispose();
+
+ var ex = Assert.Throws(() => wrapper.Connect());
+ Assert.That(ex!.ObjectName, Is.EqualTo(nameof(TcpClientWrapper)));
+ }
+
+ [Test]
+ public void Disconnect_AfterDispose_ThrowsObjectDisposedException()
+ {
+ var wrapper = new TcpClientWrapper(Host, 12345);
+ wrapper.Dispose();
+
+ Assert.Throws(() => wrapper.Disconnect());
+ }
+
+ [Test]
+ public void SendMessageAsync_AfterDispose_ThrowsObjectDisposedException()
+ {
+ var wrapper = new TcpClientWrapper(Host, 12345);
+ wrapper.Dispose();
+
+ Assert.ThrowsAsync(
+ async () => await wrapper.SendMessageAsync("hello"));
+ }
+
+ // ----------------- Connect: стара CTS -----------------
+
+ [Test]
+ public void Connect_WhenOldCancellationTokenSourceExists_CancelsAndDisposesIt()
+ {
+ var wrapper = new TcpClientWrapper(Host, 1);
+
+ var oldCts = new CancellationTokenSource();
+ SetPrivateField(wrapper, "_cts", oldCts);
+
+ Assert.DoesNotThrow(() => wrapper.Connect());
+
+ Assert.That(oldCts.IsCancellationRequested, Is.True,
+ "Old CTS must be cancelled when Connect() is called again.");
+ }
+
+ // ----------------- Disconnect: catch (Exception ex) -----------------
+
+ [Test]
+ public async Task Disconnect_WhenStreamCloseThrows_ThrowsInvalidOperationException()
+ {
+ // реальне з’єднання, щоб TcpClient.Connected == true
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ var client = new TcpClient();
+ var acceptTask = listener.AcceptTcpClientAsync();
+ await client.ConnectAsync(Host, port);
+ using var _ = await acceptTask; // тримаємо серверний бік відкритим
+
+ var wrapper = new TcpClientWrapper("dummy", 0);
+
+ var throwingStream = new ThrowOnCloseNetworkStream(client.Client);
+
+ SetPrivateField(wrapper, "_tcpClient", client);
+ SetPrivateField(wrapper, "_stream", throwingStream);
+ SetPrivateField(wrapper, "_cts", new CancellationTokenSource());
+
+ Assert.That(wrapper.Connected, Is.True, "Precondition: wrapper must be logically connected.");
+
+ var ex = Assert.Throws(() => wrapper.Disconnect());
+ Assert.That(ex!.Message, Is.EqualTo("Close boom"));
+
+ listener.Stop();
+ }
+
+ // ----------------- StartListeningAsync: успішний сценарій (MessageReceived) -----------------
+
+ [Test]
+ public async Task MessageReceived_Event_Raised_WhenServerSendsData()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ var wrapper = new TcpClientWrapper(Host, port);
+
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ wrapper.MessageReceived += (sender, data) =>
+ {
+ tcs.TrySetResult(data);
+ };
+
+ var acceptTask = listener.AcceptTcpClientAsync();
+
+ // Connect() всередині запустить StartListeningAsync(_cts.Token)
+ wrapper.Connect();
+
+ using var serverClient = await acceptTask;
+ using var serverStream = serverClient.GetStream();
+
+ var msgBytes = Encoding.UTF8.GetBytes("ping");
+ await serverStream.WriteAsync(msgBytes, 0, msgBytes.Length);
+
+ var completed = await Task.WhenAny(tcs.Task, Task.Delay(2000));
+ Assert.That(completed, Is.SameAs(tcs.Task), "MessageReceived was not raised in time.");
+
+ var received = tcs.Task.Result;
+ Assert.That(Encoding.UTF8.GetString(received), Is.EqualTo("ping"));
+
+ wrapper.Disconnect();
+ listener.Stop();
+ }
+
+ // ----------------- StartListeningAsync: catch (Exception ex) -----------------
+
+ [Test]
+ public async Task StartListeningAsync_WhenReadAsyncThrows_DoesNotPropagateException()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ var client = new TcpClient();
+ var acceptTask = listener.AcceptTcpClientAsync();
+ await client.ConnectAsync(Host, port);
+ using var _ = await acceptTask;
+
+ var wrapper = new TcpClientWrapper("dummy", 0);
+
+ var throwingStream = new ThrowOnReadNetworkStream(client.Client);
+
+ SetPrivateField(wrapper, "_tcpClient", client);
+ SetPrivateField(wrapper, "_stream", throwingStream);
+
+ var listenTask = InvokeStartListeningAsync(wrapper, CancellationToken.None);
+
+ await listenTask;
+
+ listener.Stop();
+ }
+ }
+}
+
diff --git a/NetSdrClientAppTests/UdpClientWrapperAdditionalTests.cs b/NetSdrClientAppTests/UdpClientWrapperAdditionalTests.cs
new file mode 100644
index 00000000..5f0e0451
--- /dev/null
+++ b/NetSdrClientAppTests/UdpClientWrapperAdditionalTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using NetSdrClientApp.Networking;
+using NUnit.Framework;
+
+namespace NetSdrClientAppTests.Networking;
+
+[TestFixture]
+public class UdpClientWrapperAdditionalTests
+{
+ [Test]
+ public void StartListeningAsync_AfterDispose_ThrowsObjectDisposedException()
+ {
+ // arrange
+ var client = new UdpClientWrapper(0); // порт не важливий, ми не дійдемо до створення UdpClient
+
+ client.Dispose();
+
+ // assert
+ Assert.ThrowsAsync(async () =>
+ await client.StartListeningAsync());
+ }
+
+ [Test]
+ public void Equals_And_GetHashCode_Work_ForSameAndDifferentPorts()
+ {
+ // arrange
+ var a = new UdpClientWrapper(12345);
+ var b = new UdpClientWrapper(12345);
+ var c = new UdpClientWrapper(54321);
+
+ // act + assert
+ Assert.That(a.Equals(b), Is.True, "Wrapper-и з однаковим портом мають бути рівні");
+ Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode()),
+ "HashCode для однакових параметрів має співпадати");
+
+ Assert.That(a.Equals(c), Is.False, "Wrapper-и з різними портами не повинні бути рівні");
+ }
+}
+
diff --git a/NetSdrClientAppTests/UdpClientWrapperEqualityTests.cs b/NetSdrClientAppTests/UdpClientWrapperEqualityTests.cs
new file mode 100644
index 00000000..34a8ed9e
--- /dev/null
+++ b/NetSdrClientAppTests/UdpClientWrapperEqualityTests.cs
@@ -0,0 +1,55 @@
+using System;
+using NetSdrClientApp.Networking;
+using NUnit.Framework;
+
+namespace NetSdrClientAppTests.Networking
+{
+ [TestFixture]
+ public class UdpClientWrapperEqualityTests
+ {
+ [Test]
+ public void Equals_SameReference_ReturnsTrue()
+ {
+ var wrapper = new UdpClientWrapper(60000);
+
+ Assert.That(wrapper.Equals(wrapper), Is.True);
+ }
+
+ [Test]
+ public void Equals_DifferentType_ReturnsFalse()
+ {
+ var wrapper = new UdpClientWrapper(60000);
+
+ Assert.That(wrapper.Equals("not a wrapper"), Is.False);
+ }
+
+ [Test]
+ public void Equals_And_GetHashCode_ForSameEndpoint_AreEqual()
+ {
+ var a = new UdpClientWrapper(60000);
+ var b = new UdpClientWrapper(60000);
+
+ Assert.That(a.Equals(b), Is.True);
+ Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode()));
+ }
+
+ [Test]
+ public void Equals_ForDifferentPorts_ReturnsFalse()
+ {
+ var a = new UdpClientWrapper(60000);
+ var b = new UdpClientWrapper(60001);
+
+ Assert.That(a.Equals(b), Is.False);
+ }
+
+ [Test]
+ public void StopWithoutStart_IsNoOp()
+ {
+ var wrapper = new UdpClientWrapper(60000);
+
+ Assert.DoesNotThrow(() => wrapper.StopListening());
+ Assert.DoesNotThrow(() => wrapper.Exit());
+ }
+ }
+}
+
diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs
new file mode 100644
index 00000000..c8c24980
--- /dev/null
+++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs
@@ -0,0 +1,80 @@
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using NetSdrClientApp.Networking;
+using NUnit.Framework;
+
+namespace NetSdrClientAppTests.Networking
+{
+ [TestFixture]
+ public class UdpClientWrapperTests
+ {
+ [Test]
+ public void StopListening_WhenNotStarted_DoesNotThrow()
+ {
+ var wrapper = new UdpClientWrapper(5555);
+
+ Assert.DoesNotThrow(() => wrapper.StopListening());
+ }
+
+ [Test]
+ public void Exit_WhenNotStarted_DoesNotThrow()
+ {
+ var wrapper = new UdpClientWrapper(5555);
+
+ Assert.DoesNotThrow(() => wrapper.Exit());
+ }
+
+ [Test]
+ public void GetHashCode_IsStableForSamePort_AndDifferentForOtherPort()
+ {
+ var w1 = new UdpClientWrapper(5555);
+ var w2 = new UdpClientWrapper(5555);
+ var w3 = new UdpClientWrapper(5556);
+
+ var hash1 = w1.GetHashCode();
+ var hash2 = w2.GetHashCode();
+ var hash3 = w3.GetHashCode();
+
+ Assert.AreEqual(hash1, hash2, "Hash must be stable for однакових параметрів.");
+ Assert.AreNotEqual(hash1, hash3, "Hash має відрізнятися для різних портів.");
+ }
+
+ [Test]
+ public async Task StartListeningAsync_RaisesMessageReceived_WhenPacketArrives()
+ {
+ // спочатку займаємо вільний UDP порт
+ int port;
+ using (var probe = new UdpClient(0))
+ {
+ port = ((IPEndPoint)probe.Client.LocalEndPoint!).Port;
+ }
+
+ var wrapper = new UdpClientWrapper(port);
+
+ var tcs = new TaskCompletionSource();
+ wrapper.MessageReceived += (_, data) => tcs.TrySetResult(data);
+
+ var listeningTask = wrapper.StartListeningAsync();
+
+ // відправляємо пакет на цей порт
+ using (var sender = new UdpClient())
+ {
+ var payload = new byte[] { 1, 2, 3 };
+ await sender.SendAsync(payload, payload.Length, "127.0.0.1", port);
+
+ // чекаємо максимум 1 секунду
+ var completed = await Task.WhenAny(tcs.Task, Task.Delay(1000));
+ Assert.AreSame(tcs.Task, completed, "Повідомлення не було отримано за таймаут.");
+
+ CollectionAssert.AreEqual(payload, tcs.Task.Result);
+ }
+
+ wrapper.StopListening();
+
+ // даємо задачі акуратно завершитися, але не блокуємось вічно
+ await Task.WhenAny(listeningTask, Task.Delay(1000));
+ }
+ }
+}
+
diff --git a/README.md b/README.md
index b3a90294..4a2f6c9e 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
# Лабораторні з реінжинірингу (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)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YuraKozyyr_NetSdrClient)
Цей репозиторій використовується для курсу **реінжиніринг ПЗ**.
@@ -276,3 +276,7 @@ updates:
Обмежити умову запуску Sonar: тільки PR **або** `refs/heads/master`.
- **PR зелений, push червоний**
Перевірити **New Code Definition** (Number of days або Previous version) і довести покриття/дублікації на “new code”.
+
+
+
+