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]* + + + + +