diff --git a/.github/workflows/coverlet.msbuild b/.github/workflows/coverlet.msbuild
new file mode 100644
index 00000000..5f923fc2
--- /dev/null
+++ b/.github/workflows/coverlet.msbuild
@@ -0,0 +1,3 @@
+dotnet add NetSdrClientAppTests package coverlet.msbuild
+dotnet add NetSdrClientAppTests package Microsoft.NET.Test.Sdk
+dotnet test NetSdrClientAppTests -c Release /p:CollectCoverage=true /p:CoverletOutput=TestResults/coverage.xml /p:CoverletOutputFormat=opencover
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index e7840696..d23eba70 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -1,83 +1,92 @@
-# This workflow uses actions that are not certified by GitHub.
-# They are provided by a third-party and are governed by
-# separate terms of service, privacy policy, and support
-# documentation.
-
-# This workflow helps you trigger a SonarCloud analysis of your code and populates
-# GitHub Code Scanning alerts with the vulnerabilities found.
-# Free for open source project.
-
-# 1. Login to SonarCloud.io using your GitHub account
-
-# 2. Import your project on SonarCloud
-# * Add your GitHub organization first, then add your repository as a new project.
-# * Please note that many languages are eligible for automatic analysis,
-# which means that the analysis will start automatically without the need to set up GitHub Actions.
-# * This behavior can be changed in Administration > Analysis Method.
-#
-# 3. Follow the SonarCloud in-product tutorial
-# * a. Copy/paste the Project Key and the Organization Key into the args parameter below
-# (You'll find this information in SonarCloud. Click on "Information" at the bottom left)
-#
-# * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN
-# (On SonarCloud, click on your avatar on top-right > My account > Security
-# or go directly to https://sonarcloud.io/account/security/)
-
-# Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/)
-# or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9)
-
-name: SonarCloud analysis
+name: SonarCloud Analysis
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
- workflow_dispatch:
-
-permissions:
- pull-requests: read # allows SonarCloud to decorate PRs with analysis results
jobs:
- sonar-check:
- name: Sonar Check
- runs-on: windows-latest # безпечно для будь-яких .NET проектів
+ build-and-analyze:
+ name: Build and Analyze
+ runs-on: windows-latest
+
steps:
- - uses: actions/checkout@v4
- with: { fetch-depth: 0 }
-
- - uses: actions/setup-dotnet@v4
- with:
- dotnet-version: '8.0.x'
-
- # 1) BEGIN: SonarScanner for .NET
- - name: SonarScanner Begin
- run: |
- dotnet tool install --global dotnet-sonarscanner
- echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH
- dotnet sonarscanner begin `
- /k:"ppanchen_NetSdrClient" `
- /o:"ppanchen" `
- /d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
- /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" `
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '6.0.x'
+
+ - name: Install SonarScanner
+ run: |
+ dotnet tool install --global dotnet-sonarscanner
+ echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH
+
+ - name: Install Coverlet
+ run: dotnet tool install --global coverlet.console
+
+ - name: Build and test with SonarCloud
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: |
+ # Start SonarScanner
+ dotnet sonarscanner begin `
+ /k:"Yegres546_NetSdrClient" `
+ /o:"yegres546" `
+ /d:sonar.token="$env:SONAR_TOKEN" `
+ /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" `
/d:sonar.cpd.cs.minimumTokens=40 `
/d:sonar.cpd.cs.minimumLines=5 `
- /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml `
+ /d:sonar.exclusions="**/bin/**,**/obj/**,**/TestResults/**,**/*.Tests.cs" `
+ /d:sonar.coverage.exclusions="**Test*.cs" `
/d:sonar.qualitygate.wait=true
- shell: pwsh
- # 2) BUILD & TEST
- - name: Restore
- run: dotnet restore NetSdrClient.sln
- - name: Build
- run: dotnet build NetSdrClient.sln -c Release --no-restore
- #- name: Tests with coverage (OpenCover)
- # run: |
- # dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build `
- # /p:CollectCoverage=true `
- # /p:CoverletOutput=TestResults/coverage.xml `
- # /p:CoverletOutputFormat=opencover
- # shell: pwsh
- # 3) END: SonarScanner
- - name: SonarScanner End
- run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
- shell: pwsh
+
+ # Build the solution
+ dotnet build --configuration Release
+
+ # Run tests with coverage
+ dotnet test --configuration Release `
+ --no-build `
+ --verbosity normal `
+ --collect:"XPlat Code Coverage" `
+ --results-directory ./TestResults `
+ -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
+
+ # End SonarScanner
+ dotnet sonarscanner end /d:sonar.token="$env:SONAR_TOKEN"
+name: Architecture Rules Validation
+
+on:
+ push:
+ branches: [ "lab5" ]
+ pull_request:
+ branches: [ "lab5" ]
+
+jobs:
+ architecture-tests:
+ name: Architecture Rules Validation
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '6.0.x'
+
+ - name: Install dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Run Architecture Tests
+ run: dotnet test --configuration Release --no-build --verbosity normal --filter "Category=Architecture"
+
+ - name: Run All Tests
+ run: dotnet test --configuration Release --no-build --verbosity normal
diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs
index 5966c579..0e78cbe7 100644
--- a/EchoTcpServer/Program.cs
+++ b/EchoTcpServer/Program.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
@@ -9,22 +10,24 @@
/// This program was designed for test purposes only
/// Not for a review
///
-public class EchoServer
+public class EchoServer : IDisposable
{
private readonly int _port;
private TcpListener _listener;
private CancellationTokenSource _cancellationTokenSource;
-
+ private bool _disposed = false;
public EchoServer(int port)
{
_port = port;
_cancellationTokenSource = new CancellationTokenSource();
+ _listener = new TcpListener(IPAddress.Any, _port);
}
public async Task StartAsync()
{
- _listener = new TcpListener(IPAddress.Any, _port);
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
_listener.Start();
Console.WriteLine($"Server started on port {_port}.");
@@ -42,13 +45,22 @@ public async Task StartAsync()
// Listener has been closed
break;
}
+ catch (Exception ex) when (ex is SocketException || ex is InvalidOperationException)
+ {
+ if (!_disposed)
+ {
+ Console.WriteLine($"Server error: {ex.Message}");
+ }
+ break;
+ }
}
Console.WriteLine("Server shutdown.");
}
- private async Task HandleClientAsync(TcpClient client, CancellationToken token)
+ private static async Task HandleClientAsync(TcpClient client, CancellationToken token)
{
+ using (client)
using (NetworkStream stream = client.GetStream())
{
try
@@ -56,7 +68,8 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken token)
byte[] buffer = new byte[8192];
int bytesRead;
- while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
+ while (!token.IsCancellationRequested &&
+ (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
{
// Echo back the received message
await stream.WriteAsync(buffer, 0, bytesRead, token);
@@ -69,7 +82,6 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken token)
}
finally
{
- client.Close();
Console.WriteLine("Client disconnected.");
}
}
@@ -77,97 +89,155 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken token)
public void Stop()
{
- _cancellationTokenSource.Cancel();
- _listener.Stop();
- _cancellationTokenSource.Dispose();
- Console.WriteLine("Server stopped.");
+ if (!_disposed)
+ {
+ _cancellationTokenSource.Cancel();
+ _listener.Stop();
+ Console.WriteLine("Server stopped.");
+ }
}
- public static async Task Main(string[] args)
+ protected virtual void Dispose(bool disposing)
{
- EchoServer server = new EchoServer(5000);
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _cancellationTokenSource?.Cancel();
+ _cancellationTokenSource?.Dispose();
+ _listener?.Stop();
+ // TcpListener doesn't implement IDisposable in .NET Core
+ }
- // Start the server in a separate task
- _ = Task.Run(() => server.StartAsync());
+ _disposed = true;
+ }
+ }
- string host = "127.0.0.1"; // Target IP
- int port = 60000; // Target Port
- int intervalMilliseconds = 5000; // Send every 3 seconds
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
- using (var sender = new UdpTimedSender(host, port))
+ public static async Task Main(string[] args)
+ {
+ using (var server = new EchoServer(5000))
{
- Console.WriteLine("Press any key to stop sending...");
- sender.StartSending(intervalMilliseconds);
+ // Start the server in a separate task
+ var serverTask = Task.Run(() => server.StartAsync());
- Console.WriteLine("Press 'q' to quit...");
- while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q)
+ string host = "127.0.0.1"; // Target IP
+ int port = 60000; // Target Port
+ int intervalMilliseconds = 5000; // Send every 5 seconds
+
+ using (var sender = new UdpTimedSender(host, port))
{
- // Just wait until 'q' is pressed
+ Console.WriteLine("Press any key to stop sending...");
+ sender.StartSending(intervalMilliseconds);
+
+ Console.WriteLine("Press 'q' to quit...");
+ while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q)
+ {
+ // Just wait until 'q' is pressed
+ }
+
+ sender.StopSending();
+ server.Stop();
+ Console.WriteLine("Sender stopped.");
}
- sender.StopSending();
- server.Stop();
- Console.WriteLine("Sender stopped.");
+ await serverTask;
}
}
}
-
-public class UdpTimedSender : IDisposable
+namespace EchoServerNamespace
{
- private readonly string _host;
- private readonly int _port;
- private readonly UdpClient _udpClient;
- private Timer _timer;
-
- public UdpTimedSender(string host, int port)
+ public class UdpTimedSender : IDisposable
{
- _host = host;
- _port = port;
- _udpClient = new UdpClient();
- }
+ private readonly string _host;
+ private readonly int _port;
+ private readonly UdpClient _udpClient;
+ private Timer _timer;
+ private ushort _counter = 0;
+ private readonly Random _random;
+ private bool _disposed = false;
+
+ public UdpTimedSender(string host, int port)
+ {
+ _host = host;
+ _port = port;
+ _udpClient = new UdpClient();
+ _random = new Random();
+ }
- public void StartSending(int intervalMilliseconds)
- {
- if (_timer != null)
- throw new InvalidOperationException("Sender is already running.");
+ public void StartSending(int intervalMilliseconds)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
- _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds);
- }
+ if (_timer != null)
+ throw new InvalidOperationException("Sender is already running.");
- ushort i = 0;
+ _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds);
+ }
- private void SendMessageCallback(object state)
- {
- try
+ private void SendMessageCallback(object state)
{
- //dummy data
- Random rnd = new Random();
- byte[] samples = new byte[1024];
- rnd.NextBytes(samples);
- i++;
-
- byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray();
- var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port);
+ if (_disposed) return;
- _udpClient.Send(msg, msg.Length, endpoint);
- Console.WriteLine($"Message sent to {_host}:{_port} ");
+ try
+ {
+ // Thread-safe counter increment
+ ushort currentCounter = (ushort)Interlocked.Increment(ref _counter);
+
+ // Generate dummy data
+ byte[] samples = new byte[1024];
+ _random.NextBytes(samples); // Random is thread-safe for this usage
+
+ // Create message: 0x04, 0x84 + counter + samples
+ byte[] msg = new byte[] { 0x04, 0x84 }
+ .Concat(BitConverter.GetBytes(currentCounter))
+ .Concat(samples)
+ .ToArray();
+
+ var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port);
+
+ _udpClient.Send(msg, msg.Length, endpoint);
+ Console.WriteLine($"Message #{currentCounter} sent to {_host}:{_port}");
+ }
+ catch (Exception ex)
+ {
+ if (!_disposed)
+ {
+ Console.WriteLine($"Error sending message: {ex.Message}");
+ }
+ }
}
- catch (Exception ex)
+
+ public void StopSending()
{
- Console.WriteLine($"Error sending message: {ex.Message}");
+ _timer?.Dispose();
+ _timer = null;
}
- }
- public void StopSending()
- {
- _timer?.Dispose();
- _timer = null;
- }
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ StopSending();
+ _udpClient?.Dispose();
+ }
- public void Dispose()
- {
- StopSending();
- _udpClient.Dispose();
+ _disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Infrastructure/DatabaseService.cs b/Infrastructure/DatabaseService.cs
new file mode 100644
index 00000000..2fcf4980
--- /dev/null
+++ b/Infrastructure/DatabaseService.cs
@@ -0,0 +1,10 @@
+namespace NetSdrClient.Infrastructure
+{
+ public class DatabaseService
+ {
+ public void SaveData(object data)
+ {
+ // Database operations
+ }
+ }
+}
diff --git a/Interfaces/IDeviceService.cs b/Interfaces/IDeviceService.cs
new file mode 100644
index 00000000..b19adbd7
--- /dev/null
+++ b/Interfaces/IDeviceService.cs
@@ -0,0 +1,8 @@
+namespace NetSdrClient.Interfaces
+{
+ public interface IDeviceService
+ {
+ void Connect();
+ void Disconnect();
+ }
+}
diff --git a/NetSdrClient.Tests/ArchitectureTests.cs b/NetSdrClient.Tests/ArchitectureTests.cs
new file mode 100644
index 00000000..a664fae4
--- /dev/null
+++ b/NetSdrClient.Tests/ArchitectureTests.cs
@@ -0,0 +1,148 @@
+using NetArchTest.Rules;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace NetSdrClient.Tests
+{
+ [TestClass]
+ public class ArchitectureTests
+ {
+ private const string ApplicationNamespace = "NetSdrClient";
+ private const string ModelsNamespace = "NetSdrClient.Models";
+ private const string ServicesNamespace = "NetSdrClient.Services";
+ private const string InterfacesNamespace = "NetSdrClient.Interfaces";
+ private const string InfrastructureNamespace = "NetSdrClient.Infrastructure";
+ private const string UINamespace = "NetSdrClient.UI";
+
+ [TestMethod]
+ public void ServicesLayer_ShouldNotDependOnUI()
+ {
+ // Arrange
+ var assembly = typeof(Services.SDRClient).Assembly;
+
+ // Act
+ var result = Types
+ .InAssembly(assembly)
+ .That()
+ .ResideInNamespace(ServicesNamespace)
+ .ShouldNot()
+ .HaveDependencyOn(UINamespace)
+ .GetResult();
+
+ // Assert
+ Assert.IsTrue(result.IsSuccessful,
+ $"Services layer should not depend on UI: {string.Join(", ", result.FailingTypes)}");
+ }
+
+ [TestMethod]
+ public void Models_ShouldNotReferenceServices()
+ {
+ // Arrange
+ var assembly = typeof(Models.SDRDevice).Assembly;
+
+ // Act
+ var result = Types
+ .InAssembly(assembly)
+ .That()
+ .ResideInNamespace(ModelsNamespace)
+ .ShouldNot()
+ .HaveDependencyOn(ServicesNamespace)
+ .GetResult();
+
+ // Assert
+ Assert.IsTrue(result.IsSuccessful,
+ $"Models should not depend on Services: {string.Join(", ", result.FailingTypes)}");
+ }
+
+ [TestMethod]
+ public void Interfaces_ShouldNotHaveDependencies()
+ {
+ // Arrange
+ var assembly = typeof(Services.SDRClient).Assembly;
+
+ // Act
+ var result = Types
+ .InAssembly(assembly)
+ .That()
+ .ResideInNamespace(InterfacesNamespace)
+ .Should()
+ .NotHaveDependencyOnAny(
+ ServicesNamespace,
+ ModelsNamespace,
+ InfrastructureNamespace,
+ UINamespace)
+ .GetResult();
+
+ // Assert
+ Assert.IsTrue(result.IsSuccessful,
+ $"Interfaces should not have dependencies: {string.Join(", ", result.FailingTypes)}");
+ }
+
+ [TestMethod]
+ public void AllClasses_ShouldHaveNamesEndingWithService_IfInServicesNamespace()
+ {
+ // Arrange
+ var assembly = typeof(Services.SDRClient).Assembly;
+
+ // Act
+ var result = Types
+ .InAssembly(assembly)
+ .That()
+ .ResideInNamespace(ServicesNamespace)
+ .And()
+ .AreClasses()
+ .Should()
+ .HaveNameEndingWith("Service")
+ .Or()
+ .HaveNameEndingWith("Client")
+ .Or()
+ .HaveNameEndingWith("Handler")
+ .GetResult();
+
+ // Assert
+ Assert.IsTrue(result.IsSuccessful,
+ $"Services should have proper naming: {string.Join(", ", result.FailingTypes)}");
+ }
+
+ [TestMethod]
+ public void Models_ShouldBeSealed()
+ {
+ // Arrange
+ var assembly = typeof(Models.SDRDevice).Assembly;
+
+ // Act
+ var result = Types
+ .InAssembly(assembly)
+ .That()
+ .ResideInNamespace(ModelsNamespace)
+ .And()
+ .AreClasses()
+ .Should()
+ .BeSealed()
+ .GetResult();
+
+ // Assert
+ Assert.IsTrue(result.IsSuccessful,
+ $"Models should be sealed: {string.Join(", ", result.FailingTypes)}");
+ }
+
+ [TestMethod]
+ public void Services_ShouldNotDependOnInfrastructureDirectly()
+ {
+ // Arrange
+ var assembly = typeof(Services.SDRClient).Assembly;
+
+ // Act
+ var result = Types
+ .InAssembly(assembly)
+ .That()
+ .ResideInNamespace(ServicesNamespace)
+ .ShouldNot()
+ .HaveDependencyOn(InfrastructureNamespace)
+ .GetResult();
+
+ // Assert
+ Assert.IsTrue(result.IsSuccessful,
+ $"Services should not depend directly on Infrastructure: {string.Join(", ", result.FailingTypes)}");
+ }
+ }
+}
diff --git a/NetSdrClient.Tests/DisposeTests.cs b/NetSdrClient.Tests/DisposeTests.cs
new file mode 100644
index 00000000..add7ee6f
--- /dev/null
+++ b/NetSdrClient.Tests/DisposeTests.cs
@@ -0,0 +1,92 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NetSdrClient.Services;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NetSdrClient.Tests
+{
+ [TestClass]
+ public class DisposeTests
+ {
+ [TestMethod]
+ public void SDRClient_Dispose_ShouldCancelCancellationToken()
+ {
+ // Arrange
+ var client = new SDRClient();
+
+ // Act
+ client.Dispose();
+
+ // Assert
+ // Перевіряємо, що токен скасований після Dispose
+ Assert.IsTrue(client.IsDisposed);
+ }
+
+ [TestMethod]
+ public async Task SDRClient_DisposeDuringOperation_ShouldCancelOperation()
+ {
+ // Arrange
+ var client = new SDRClient();
+ var operationCompleted = false;
+
+ // Act
+ var task = Task.Run(async () =>
+ {
+ try
+ {
+ await client.StartAsync();
+ operationCompleted = true;
+ }
+ catch (OperationCanceledException)
+ {
+ // Очікувана поведінка
+ }
+ });
+
+ // Даємо трохи часу на початок операції
+ await Task.Delay(100);
+
+ // Dispose має скасувати операцію
+ client.Dispose();
+
+ await task;
+
+ // Assert
+ Assert.IsFalse(operationCompleted, "Operation should be cancelled by Dispose");
+ }
+
+ [TestMethod]
+ public void SDRClient_MultipleDispose_ShouldNotThrowException()
+ {
+ // Arrange
+ var client = new SDRClient();
+
+ // Act & Assert
+ try
+ {
+ client.Dispose();
+ client.Dispose(); // Другий виклик не має кидати виняток
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Multiple Dispose calls should not throw exception: {ex.Message}");
+ }
+ }
+
+ [TestMethod]
+ public void CancellationTokenSource_InUsingBlock_ShouldBeDisposedAutomatically()
+ {
+ // Arrange & Act & Assert
+ // Не має бути помилок або попереджень
+ using (var cts = new CancellationTokenSource())
+ {
+ var token = cts.Token;
+ cts.CancelAfter(100);
+
+ Assert.IsTrue(token.CanBeCanceled);
+ }
+ // cts автоматично видаляється тут
+ }
+ }
+}
diff --git a/NetSdrClient.Tests/NetSdrClient.Tests.csproj b/NetSdrClient.Tests/NetSdrClient.Tests.csproj
new file mode 100644
index 00000000..7697a375
--- /dev/null
+++ b/NetSdrClient.Tests/NetSdrClient.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NetSdrClient.Tests/NetworkServiceTests.cs b/NetSdrClient.Tests/NetworkServiceTests.cs
new file mode 100644
index 00000000..3d29fd9e
--- /dev/null
+++ b/NetSdrClient.Tests/NetworkServiceTests.cs
@@ -0,0 +1,75 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NetSdrClient.Services;
+
+namespace NetSdrClient.Tests
+{
+ [TestClass]
+ public class NetworkServiceTests
+ {
+ [TestMethod]
+ public void NetworkService_Constructor_ShouldInitialize()
+ {
+ // Arrange & Act
+ var service = new NetworkService();
+
+ // Assert
+ Assert.IsNotNull(service);
+ }
+
+ [TestMethod]
+ public void IsValidIpAddress_WithValidIp_ShouldReturnTrue()
+ {
+ // Arrange
+ var service = new NetworkService();
+ var validIp = "192.168.1.1";
+
+ // Act
+ var result = service.IsValidIpAddress(validIp);
+
+ // Assert
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void IsValidIpAddress_WithInvalidIp_ShouldReturnFalse()
+ {
+ // Arrange
+ var service = new NetworkService();
+ var invalidIp = "999.999.999.999";
+
+ // Act
+ var result = service.IsValidIpAddress(invalidIp);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void IsValidPort_WithValidPort_ShouldReturnTrue()
+ {
+ // Arrange
+ var service = new NetworkService();
+ var validPort = 8080;
+
+ // Act
+ var result = service.IsValidPort(validPort);
+
+ // Assert
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void IsValidPort_WithInvalidPort_ShouldReturnFalse()
+ {
+ // Arrange
+ var service = new NetworkService();
+ var invalidPort = 99999;
+
+ // Act
+ var result = service.IsValidPort(invalidPort);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+ }
+}
diff --git a/NetSdrClient.Tests/ProtocolHandlerTests.cs b/NetSdrClient.Tests/ProtocolHandlerTests.cs
new file mode 100644
index 00000000..60b17bc4
--- /dev/null
+++ b/NetSdrClient.Tests/ProtocolHandlerTests.cs
@@ -0,0 +1,63 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NetSdrClient.Services;
+
+namespace NetSdrClient.Tests
+{
+ [TestClass]
+ public class ProtocolHandlerTests
+ {
+ [TestMethod]
+ public void ProtocolHandler_Constructor_ShouldInitialize()
+ {
+ // Arrange & Act
+ var handler = new ProtocolHandler();
+
+ // Assert
+ Assert.IsNotNull(handler);
+ }
+
+ [TestMethod]
+ public void CreateCommand_ShouldReturnValidCommand()
+ {
+ // Arrange
+ var handler = new ProtocolHandler();
+ var expectedCommand = "CONNECT";
+
+ // Act
+ var result = handler.CreateCommand(expectedCommand);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.Contains(expectedCommand));
+ }
+
+ [TestMethod]
+ public void ParseResponse_WithValidData_ShouldReturnParsedResponse()
+ {
+ // Arrange
+ var handler = new ProtocolHandler();
+ var testData = "OK:CONNECTED";
+
+ // Act
+ var result = handler.ParseResponse(testData);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.Contains("CONNECTED"));
+ }
+
+ [TestMethod]
+ public void ParseResponse_WithNullData_ShouldReturnErrorMessage()
+ {
+ // Arrange
+ var handler = new ProtocolHandler();
+
+ // Act
+ var result = handler.ParseResponse(null);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.Contains("ERROR"));
+ }
+ }
+}
diff --git a/NetSdrClient.Tests/SDRClientTests.cs b/NetSdrClient.Tests/SDRClientTests.cs
new file mode 100644
index 00000000..05cb679d
--- /dev/null
+++ b/NetSdrClient.Tests/SDRClientTests.cs
@@ -0,0 +1,97 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NetSdrClient.Models;
+using NetSdrClient.Services;
+using System.Linq;
+
+namespace NetSdrClient.Tests
+{
+ [TestClass]
+ public class SDRClientTests
+ {
+ [TestMethod]
+ public void SDRClient_Constructor_ShouldInitializeDevicesList()
+ {
+ // Arrange & Act
+ var client = new SDRClient();
+
+ // Assert
+ Assert.IsNotNull(client.Devices);
+ Assert.AreEqual(0, client.Devices.Count);
+ }
+
+ [TestMethod]
+ public void AddDevice_ShouldAddDeviceToList()
+ {
+ // Arrange
+ var client = new SDRClient();
+ var device = new SDRDevice { Id = 1, Name = "Test Device" };
+
+ // Act
+ client.AddDevice(device);
+
+ // Assert
+ Assert.AreEqual(1, client.Devices.Count);
+ Assert.AreEqual(device, client.Devices[0]);
+ }
+
+ [TestMethod]
+ public void RemoveDevice_ShouldRemoveDeviceFromList()
+ {
+ // Arrange
+ var client = new SDRClient();
+ var device = new SDRDevice { Id = 1, Name = "Test Device" };
+ client.AddDevice(device);
+
+ // Act
+ var result = client.RemoveDevice(1);
+
+ // Assert
+ Assert.IsTrue(result);
+ Assert.AreEqual(0, client.Devices.Count);
+ }
+
+ [TestMethod]
+ public void RemoveDevice_WithInvalidId_ShouldReturnFalse()
+ {
+ // Arrange
+ var client = new SDRClient();
+
+ // Act
+ var result = client.RemoveDevice(999);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void GetDevice_ShouldReturnCorrectDevice()
+ {
+ // Arrange
+ var client = new SDRClient();
+ var device1 = new SDRDevice { Id = 1, Name = "Device 1" };
+ var device2 = new SDRDevice { Id = 2, Name = "Device 2" };
+ client.AddDevice(device1);
+ client.AddDevice(device2);
+
+ // Act
+ var result = client.GetDevice(2);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual("Device 2", result.Name);
+ }
+
+ [TestMethod]
+ public void GetDevice_WithInvalidId_ShouldReturnNull()
+ {
+ // Arrange
+ var client = new SDRClient();
+
+ // Act
+ var result = client.GetDevice(999);
+
+ // Assert
+ Assert.IsNull(result);
+ }
+ }
+}
diff --git a/NetSdrClient.Tests/SDRDeviceTests.cs b/NetSdrClient.Tests/SDRDeviceTests.cs
new file mode 100644
index 00000000..89295dd1
--- /dev/null
+++ b/NetSdrClient.Tests/SDRDeviceTests.cs
@@ -0,0 +1,55 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NetSdrClient.Models;
+
+namespace NetSdrClient.Tests
+{
+ [TestClass]
+ public class SDRDeviceTests
+ {
+ [TestMethod]
+ public void SDRDevice_Constructor_ShouldInitializeProperties()
+ {
+ // Arrange & Act
+ var device = new SDRDevice();
+
+ // Assert
+ Assert.IsNotNull(device);
+ Assert.AreEqual(0, device.Id);
+ Assert.IsNull(device.Name);
+ Assert.IsNull(device.Description);
+ Assert.IsFalse(device.IsConnected);
+ }
+
+ [TestMethod]
+ public void SDRDevice_Properties_ShouldSetAndGetCorrectly()
+ {
+ // Arrange
+ var device = new SDRDevice();
+
+ // Act
+ device.Id = 1;
+ device.Name = "Test Device";
+ device.Description = "Test Description";
+ device.IsConnected = true;
+
+ // Assert
+ Assert.AreEqual(1, device.Id);
+ Assert.AreEqual("Test Device", device.Name);
+ Assert.AreEqual("Test Description", device.Description);
+ Assert.IsTrue(device.IsConnected);
+ }
+
+ [TestMethod]
+ public void SDRDevice_ToString_ShouldReturnName()
+ {
+ // Arrange
+ var device = new SDRDevice { Name = "Test SDR" };
+
+ // Act
+ var result = device.ToString();
+
+ // Assert
+ Assert.AreEqual("Test SDR", result);
+ }
+ }
+}
diff --git a/NetSdrClient/Services/BadService.cs b/NetSdrClient/Services/BadService.cs
new file mode 100644
index 00000000..51da3ba7
--- /dev/null
+++ b/NetSdrClient/Services/BadService.cs
@@ -0,0 +1,20 @@
+using NetSdrClient.Models;
+using NetSdrClient.UI; // Навмисне порушення - Services залежить від UI
+
+namespace NetSdrClient.Services
+{
+ public class BadService
+ {
+ private readonly UIComponent _uiComponent; // Порушення!
+
+ public BadService()
+ {
+ _uiComponent = new UIComponent();
+ }
+
+ public void DoSomething()
+ {
+ _uiComponent.ShowMessage("This violates architecture rules!");
+ }
+ }
+}
diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs
index b0a7c058..d4466ac5 100644
--- a/NetSdrClientApp/NetSdrClient.cs
+++ b/NetSdrClientApp/NetSdrClient.cs
@@ -9,7 +9,29 @@
using System.Threading.Tasks;
using static NetSdrClientApp.Messages.NetSdrMessageHelper;
using static System.Runtime.InteropServices.JavaScript.JSType;
+using NetSdrClient.Models;
+using NetSdrClient.Interfaces;
+namespace NetSdrClient.Services
+{
+ public class SDRClient : IDeviceService
+ {
+ public List Devices { get; private set; }
+
+ // Реалізація інтерфейсу
+ public void Connect()
+ {
+ // Connection logic
+ }
+
+ public void Disconnect()
+ {
+ // Disconnection logic
+ }
+
+ // Інші методи...
+ }
+}
namespace NetSdrClientApp
{
public class NetSdrClient
diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs
index 1f37e2e5..62dd5c1e 100644
--- a/NetSdrClientApp/Networking/TcpClientWrapper.cs
+++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -10,15 +10,16 @@
namespace NetSdrClientApp.Networking
{
- public class TcpClientWrapper : ITcpClient
+ public class TcpClientWrapper : ITcpClient, IDisposable
{
private string _host;
private int _port;
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private CancellationTokenSource _cts;
+ private bool _disposed = false;
- public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null;
+ public bool Connected => !_disposed && _tcpClient != null && _tcpClient.Connected && _stream != null;
public event EventHandler? MessageReceived;
@@ -26,10 +27,13 @@ public TcpClientWrapper(string host, int port)
{
_host = host;
_port = port;
+ _cts = new CancellationTokenSource();
}
public void Connect()
{
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
if (Connected)
{
Console.WriteLine($"Already connected to {_host}:{_port}");
@@ -40,7 +44,6 @@ public void Connect()
try
{
- _cts = new CancellationTokenSource();
_tcpClient.Connect(_host, _port);
_stream = _tcpClient.GetStream();
Console.WriteLine($"Connected to {_host}:{_port}");
@@ -49,6 +52,8 @@ public void Connect()
catch (Exception ex)
{
Console.WriteLine($"Failed to connect: {ex.Message}");
+ CleanupResources();
+ throw;
}
}
@@ -57,12 +62,7 @@ public void Disconnect()
if (Connected)
{
_cts?.Cancel();
- _stream?.Close();
- _tcpClient?.Close();
-
- _cts = null;
- _tcpClient = null;
- _stream = null;
+ CleanupResources();
Console.WriteLine("Disconnected.");
}
else
@@ -73,6 +73,8 @@ public void Disconnect()
public async Task SendMessageAsync(byte[] data)
{
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
if (Connected && _stream != null && _stream.CanWrite)
{
Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}"));
@@ -86,6 +88,8 @@ public async Task SendMessageAsync(byte[] data)
public async Task SendMessageAsync(string str)
{
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
var data = Encoding.UTF8.GetBytes(str);
if (Connected && _stream != null && _stream.CanWrite)
{
@@ -104,7 +108,7 @@ private async Task StartListeningAsync()
{
try
{
- Console.WriteLine($"Starting listening for incomming messages.");
+ Console.WriteLine($"Starting listening for incoming messages.");
while (!_cts.Token.IsCancellationRequested)
{
@@ -117,13 +121,16 @@ private async Task StartListeningAsync()
}
}
}
- catch (OperationCanceledException ex)
+ catch (OperationCanceledException)
{
- //empty
+ // Expected when cancellation is requested
}
catch (Exception ex)
{
- Console.WriteLine($"Error in listening loop: {ex.Message}");
+ if (!_disposed)
+ {
+ Console.WriteLine($"Error in listening loop: {ex.Message}");
+ }
}
finally
{
@@ -135,6 +142,38 @@ private async Task StartListeningAsync()
throw new InvalidOperationException("Not connected to a server.");
}
}
- }
+ private void CleanupResources()
+ {
+ _stream?.Close();
+ _stream?.Dispose();
+ _stream = null;
+
+ _tcpClient?.Close();
+ _tcpClient?.Dispose();
+ _tcpClient = null;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ // Dispose managed resources
+ _cts?.Cancel();
+ _cts?.Dispose();
+ CleanupResources();
+ }
+
+ _disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
}
diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs
index 31e0b798..b1e35c03 100644
--- a/NetSdrClientApp/Networking/UdpClientWrapper.cs
+++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
@@ -6,22 +6,25 @@
using System.Threading;
using System.Threading.Tasks;
-public class UdpClientWrapper : IUdpClient
+public class UdpClientWrapper : IUdpClient, IDisposable
{
private readonly IPEndPoint _localEndPoint;
- private CancellationTokenSource? _cts;
+ private CancellationTokenSource _cts;
private UdpClient? _udpClient;
+ private bool _disposed = false;
public event EventHandler? MessageReceived;
public UdpClientWrapper(int port)
{
_localEndPoint = new IPEndPoint(IPAddress.Any, port);
+ _cts = new CancellationTokenSource();
}
public async Task StartListeningAsync()
{
- _cts = new CancellationTokenSource();
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
Console.WriteLine("Start listening for UDP messages...");
try
@@ -35,18 +38,27 @@ public async Task StartListeningAsync()
Console.WriteLine($"Received from {result.RemoteEndPoint}");
}
}
- catch (OperationCanceledException ex)
+ catch (OperationCanceledException)
+ {
+ // Expected when cancellation is requested
+ }
+ catch (ObjectDisposedException)
{
- //empty
+ // Expected when UdpClient is disposed
}
catch (Exception ex)
{
- Console.WriteLine($"Error receiving message: {ex.Message}");
+ if (!_disposed)
+ {
+ Console.WriteLine($"Error receiving message: {ex.Message}");
+ }
}
}
public void StopListening()
{
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
try
{
_cts?.Cancel();
@@ -55,31 +67,62 @@ public void StopListening()
}
catch (Exception ex)
{
- Console.WriteLine($"Error while stopping: {ex.Message}");
+ if (!_disposed)
+ {
+ Console.WriteLine($"Error while stopping: {ex.Message}");
+ }
}
}
public void Exit()
+ {
+ // Exit - це по суті те саме що StopListening, але для узгодженості
+ 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);
+ }
+
+ private void CleanupResources()
{
try
{
_cts?.Cancel();
_udpClient?.Close();
- Console.WriteLine("Stopped listening for UDP messages.");
+ _udpClient?.Dispose();
+ _udpClient = null;
}
catch (Exception ex)
{
- Console.WriteLine($"Error while stopping: {ex.Message}");
+ Console.WriteLine($"Error during cleanup: {ex.Message}");
}
}
- public override int GetHashCode()
+ protected virtual void Dispose(bool disposing)
{
- var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}";
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ // Dispose managed resources
+ CleanupResources();
+ _cts?.Dispose();
+ }
- using var md5 = MD5.Create();
- var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload));
+ _disposed = true;
+ }
+ }
- return BitConverter.ToInt32(hash, 0);
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
}
-}
\ No newline at end of file
+}
diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj
index 3cbc46af..e1f5e652 100644
--- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj
+++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj
@@ -1,17 +1,18 @@
-
net8.0
enable
enable
-
false
true
-
-
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
@@ -19,11 +20,10 @@
-
+
-
diff --git a/UI/UIComponent.cs b/UI/UIComponent.cs
new file mode 100644
index 00000000..347ef573
--- /dev/null
+++ b/UI/UIComponent.cs
@@ -0,0 +1,10 @@
+namespace NetSdrClient.UI
+{
+ public class UIComponent
+ {
+ public void ShowMessage(string message)
+ {
+ // UI logic here - this should not be called from Services
+ }
+ }
+}
diff --git a/coverlet.msbuild b/coverlet.msbuild
new file mode 100644
index 00000000..5f923fc2
--- /dev/null
+++ b/coverlet.msbuild
@@ -0,0 +1,3 @@
+dotnet add NetSdrClientAppTests package coverlet.msbuild
+dotnet add NetSdrClientAppTests package Microsoft.NET.Test.Sdk
+dotnet test NetSdrClientAppTests -c Release /p:CollectCoverage=true /p:CoverletOutput=TestResults/coverage.xml /p:CoverletOutputFormat=opencover
diff --git a/coverlet.runsettings b/coverlet.runsettings
new file mode 100644
index 00000000..9815d295
--- /dev/null
+++ b/coverlet.runsettings
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ opencover
+ [NetSdrClient]*
+ [NetSdrClient.Tests]*
+
+
+
+
+