diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e7840696..abe18728 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,31 +1,3 @@ -# 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 on: @@ -36,15 +8,16 @@ on: workflow_dispatch: permissions: - pull-requests: read # allows SonarCloud to decorate PRs with analysis results + pull-requests: read jobs: sonar-check: name: Sonar Check - runs-on: windows-latest # безпечно для будь-яких .NET проектів + runs-on: windows-latest steps: - uses: actions/checkout@v4 - with: { fetch-depth: 0 } + with: + fetch-depth: 0 - uses: actions/setup-dotnet@v4 with: @@ -54,29 +27,33 @@ jobs: - name: SonarScanner Begin run: | dotnet tool install --global dotnet-sonarscanner - echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` - /k:"ppanchen_NetSdrClient" ` - /o:"ppanchen" ` - /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` - /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` - /d:sonar.cpd.cs.minimumTokens=40 ` - /d:sonar.cpd.cs.minimumLines=5 ` - /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` - /d:sonar.qualitygate.wait=true + /k:"NatashaTymchenko_NetSdrClient" ` + /o:"natashatymchenko" ` + /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` + /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` + /d:sonar.cpd.cs.minimumTokens=40 ` + /d:sonar.cpd.cs.minimumLines=5 ` + /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` + /d:sonar.qualitygate.wait=true shell: pwsh - # 2) BUILD & TEST + + # 2) BUILD - 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 + + # ДОДАНО БЛОК ТЕСТІВ + - 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 }}" diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs new file mode 100644 index 00000000..f82cc23c --- /dev/null +++ b/EchoTcpServer/EchoServer.cs @@ -0,0 +1,80 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoTcpServer +{ + public class EchoServer + { + private TcpListener? _listener; + private CancellationTokenSource? _cts; + + public int Port { get; private set; } + public bool IsRunning { get; private set; } + + public void Start(int port) + { + if (IsRunning) return; + + Port = port; + _listener = new TcpListener(IPAddress.Any, Port); + _listener.Start(); + IsRunning = true; + _cts = new CancellationTokenSource(); + + Console.WriteLine($"Server started on port {Port}."); + + Task.Run(() => ListenLoopAsync(_cts.Token)); + } + + public void Stop() + { + if (!IsRunning) return; + + _cts?.Cancel(); + _listener?.Stop(); + IsRunning = false; + Console.WriteLine("Server stopped."); + } + + private async Task ListenLoopAsync(CancellationToken token) + { + try + { + while (!token.IsCancellationRequested && _listener != null) + { + var client = await _listener.AcceptTcpClientAsync(token); + Console.WriteLine("Client connected."); + _ = HandleClientAsync(client, token); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + Console.WriteLine($"Listen error: {ex.Message}"); + } + } + + private async Task HandleClientAsync(TcpClient client, CancellationToken token) + { + using (client) + using (var stream = client.GetStream()) + { + var buffer = new byte[8192]; + int bytesRead; + try + { + while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + + await stream.WriteAsync(buffer, 0, bytesRead, token); + Console.WriteLine($"Echoed {bytesRead} bytes."); + } + } + catch { /* Ігноруємо помилки при розриві з'єднання */ } + finally { Console.WriteLine("Client disconnected."); } + } + } + } +} diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c579..166a5ba2 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,173 +1,32 @@ using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; using System.Threading.Tasks; -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer +namespace EchoTcpServer { - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; - - - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task StartAsync() - { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - try + class Program + { + static async Task Main(string[] args) + { + var server = new EchoServer(); + server.Start(5000); + string host = "127.0.0.1"; + int port = 60000; + int intervalMilliseconds = 3000; + using (var sender = new UdpTimedSender(host, port)) { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); + Console.WriteLine("Press any key to start sending UDP..."); + Console.ReadKey(); + + sender.StartSending(intervalMilliseconds); - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } - - Console.WriteLine("Server shutdown."); - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try - { - byte[] buffer = new byte[8192]; - int bytesRead; - - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + Console.WriteLine("Press 'Q' to stop server and quit..."); + while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { - // Echo back the received message - await stream.WriteAsync(buffer, 0, bytesRead, token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - Console.WriteLine($"Error: {ex.Message}"); - } - finally - { - client.Close(); - Console.WriteLine("Client disconnected."); - } - } - } - - public void Stop() - { - _cancellationTokenSource.Cancel(); - _listener.Stop(); - _cancellationTokenSource.Dispose(); - Console.WriteLine("Server stopped."); - } - - public static async Task Main(string[] args) - { - EchoServer server = new EchoServer(5000); - // Start the server in a separate task - _ = Task.Run(() => server.StartAsync()); - - string host = "127.0.0.1"; // Target IP - int port = 60000; // Target Port - int intervalMilliseconds = 5000; // Send every 3 seconds - - using (var sender = new UdpTimedSender(host, port)) - { - Console.WriteLine("Press any key to stop sending..."); - sender.StartSending(intervalMilliseconds); - - Console.WriteLine("Press 'q' to quit..."); - while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) - { - // Just wait until 'q' is pressed + sender.StopSending(); + server.Stop(); } - - sender.StopSending(); - server.Stop(); - Console.WriteLine("Sender stopped."); } } } - - -public class UdpTimedSender : IDisposable -{ - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private Timer _timer; - - public UdpTimedSender(string host, int port) - { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } - - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); - - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } - - ushort i = 0; - - private void SendMessageCallback(object state) - { - try - { - //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); - - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port} "); - } - catch (Exception ex) - { - Console.WriteLine($"Error sending message: {ex.Message}"); - } - } - - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } - - public void Dispose() - { - StopSending(); - _udpClient.Dispose(); - } -} \ No newline at end of file diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs new file mode 100644 index 00000000..170f7567 --- /dev/null +++ b/EchoTcpServer/UdpTimedSender.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace EchoTcpServer +{ + public class UdpTimedSender : IDisposable + { + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private Timer? _timer; + private ushort _counter = 0; + + public UdpTimedSender(string host, int port) + { + _host = host; + _port = port; + _udpClient = new UdpClient(); + } + + public void StartSending(int intervalMilliseconds) + { + if (_timer != null) return; + _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); + } + + private void SendMessageCallback(object? state) + { + try + { + Random rnd = new Random(); + byte[] samples = new byte[1024]; + rnd.NextBytes(samples); + _counter++; + + byte[] msg = (new byte[] { 0x04, 0x84 }) + .Concat(BitConverter.GetBytes(_counter)) + .Concat(samples).ToArray(); + + var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); + _udpClient.Send(msg, msg.Length, endpoint); + Console.WriteLine($"UDP Message sent to {_host}:{_port}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error sending message: {ex.Message}"); + } + } + + public void StopSending() + { + _timer?.Dispose(); + _timer = null; + } + + public void Dispose() + { + StopSending(); + _udpClient.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/EchoTcpServerTests/EchoTcpServerTests/EchoTcpServerTests.cs b/EchoTcpServerTests/EchoTcpServerTests/EchoTcpServerTests.cs new file mode 100644 index 00000000..1fc365d2 --- /dev/null +++ b/EchoTcpServerTests/EchoTcpServerTests/EchoTcpServerTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using EchoTcpServer; +using System.Threading.Tasks; + +namespace EchoTcpServerTests +{ + [TestFixture] + public class EchoServerTests + { + private EchoServer _server; + + [SetUp] + public void Setup() + { + _server = new EchoServer(); + } + + [TearDown] + public void Teardown() + { + _server.Stop(); + } + + [Test] + public void Start_ShouldSetIsRunningToTrue() + { + _server.Start(0); + + Assert.IsTrue(_server.IsRunning, "Server should have IsRunning = true"); + Assert.AreNotEqual(0, _server.Port, "Port should be assigned automatically"); + } + + [Test] + public void Stop_ShouldSetIsRunningToFalse() + { + _server.Start(0); + + _server.Stop(); + + Assert.IsFalse(_server.IsRunning, "Server should have IsRunning = false"); + } + } +} diff --git a/EchoTcpServerTests/EchoTcpServerTests/EchoTcpServerTests.csproj b/EchoTcpServerTests/EchoTcpServerTests/EchoTcpServerTests.csproj new file mode 100644 index 00000000..fefbdd3b --- /dev/null +++ b/EchoTcpServerTests/EchoTcpServerTests/EchoTcpServerTests.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + diff --git a/NetSdrClientApp/Networking/ITcpClient.cs b/NetSdrClientApp/Networking/ITcpClient.cs index 3470b5d7..05ab5144 100644 --- a/NetSdrClientApp/Networking/ITcpClient.cs +++ b/NetSdrClientApp/Networking/ITcpClient.cs @@ -1,19 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Threading.Tasks; namespace NetSdrClientApp.Networking { - public interface ITcpClient + public interface ITcpClient: System.IDisposable { - void Connect(); - void Disconnect(); - Task SendMessageAsync(byte[] data); - - event EventHandler MessageReceived; - public bool Connected { get; } + bool Connected { get; } + Task ConnectAsync(); + void Disconnect(); + Task WriteAsync(byte[] data); } } diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs index 1b9f9311..bbe00575 100644 --- a/NetSdrClientApp/Networking/IUdpClient.cs +++ b/NetSdrClientApp/Networking/IUdpClient.cs @@ -1,10 +1,10 @@ - -public interface IUdpClient -{ - event EventHandler? MessageReceived; +using System.Threading.Tasks; - Task StartListeningAsync(); - - void StopListening(); - void Exit(); -} \ No newline at end of file +namespace NetSdrClientApp.Networking +{ + public interface IUdpClient + { + Task StartListeningAsync(); + void StopListening(); + } +} diff --git a/NetSdrClientAppTests/ArchitectureTests.cs b/NetSdrClientAppTests/ArchitectureTests.cs new file mode 100644 index 00000000..c6188d30 --- /dev/null +++ b/NetSdrClientAppTests/ArchitectureTests.cs @@ -0,0 +1,24 @@ +using NUnit.Framework; +using NetArchTest.Rules; +using NetSdrClientApp.Networking; + +namespace NetSdrClientAppTests +{ + public class ArchitectureTests + { + [Test] + public void Networking_Classes_Should_Have_Service_Suffix() + { + var result = Types.InAssembly(typeof(TcpClientWrapper).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Networking") + .And() + .AreClasses() + .Should() + .HaveNameEndingWith("Wrapper") + .GetResult(); + + Assert.IsTrue(result.IsSuccessful, "Architecture violation: Networking classes must end with 'Service'"); + } + } +} diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46af..fd007ae8 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -16,6 +16,7 @@ + diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f8..964a805f 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -1,119 +1,57 @@ -using Moq; +using NUnit.Framework; +using Moq; using NetSdrClientApp; using NetSdrClientApp.Networking; +using System.Threading.Tasks; -namespace NetSdrClientAppTests; - -public class NetSdrClientTests +namespace NetSdrClientAppTests { - NetSdrClient _client; - Mock _tcpMock; - Mock _updMock; - - public NetSdrClientTests() { } - - [SetUp] - public void Setup() + [TestFixture] + public class NetSdrClientTests { - _tcpMock = new Mock(); - _tcpMock.Setup(tcp => tcp.Connect()).Callback(() => - { - _tcpMock.Setup(tcp => tcp.Connected).Returns(true); - }); + private Mock _mockTcp; + private Mock _mockUdp; + private NetSdrClient _client; - _tcpMock.Setup(tcp => tcp.Disconnect()).Callback(() => + [SetUp] + public void Setup() { - _tcpMock.Setup(tcp => tcp.Connected).Returns(false); - }); + _mockTcp = new Mock(); + _mockUdp = new Mock(); + _client = new NetSdrClient(_mockTcp.Object, _mockUdp.Object); + } - _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())).Callback((bytes) => + [Test] + public async Task ConnectAsync_ShouldCallTcpConnect() { - _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes); - }); - - _updMock = new Mock(); - - _client = new NetSdrClient(_tcpMock.Object, _updMock.Object); - } - - [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)); - } - - [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); - } - - [Test] - public async Task StartIQTest() - { - //Arrange - await ConnectAsyncTest(); + + await _client.ConnectAsync(); + + _mockTcp.Verify(x => x.ConnectAsync(), Times.AtLeastOnce); + } + + [Test] + public void Disconnect_ShouldCallTcpDisconnect() + { + _client.Disconnect(); // Тут вже правильна назва! - //act - await _client.StartIQAsync(); + _mockTcp.Verify(x => x.Disconnect(), Times.Once); + } - //assert - //No exception thrown - _updMock.Verify(udp => udp.StartListeningAsync(), Times.Once); - Assert.That(_client.IQStarted, Is.True); - } + [Test] + public async Task StartIQAsync_ShouldSendCorrectCommand() + { + await _client.StartIQAsync(); - [Test] - public async Task StopIQTest() - { - //Arrange - await ConnectAsyncTest(); + _mockTcp.Verify(x => x.WriteAsync(It.IsAny()), Times.AtLeastOnce); + } - //act - await _client.StopIQAsync(); + [Test] + public async Task ChangeFrequencyAsync_ShouldSendBytes() + { + await _client.ChangeFrequencyAsync(1000000, 1); - //assert - //No exception thrown - _updMock.Verify(tcp => tcp.StopListening(), Times.Once); - Assert.That(_client.IQStarted, Is.False); + _mockTcp.Verify(x => x.WriteAsync(It.IsAny()), Times.AtLeastOnce); + } } - - //TODO: cover the rest of the NetSdrClient code here } diff --git a/README.md b/README.md index b3a90294..4f361899 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=NatashaTymchenko_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=NatashaTymchenko_NetSdrClient) # Лабораторні з реінжинірингу (8×) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)