diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ae591b1 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..446b951 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e784069..0b30ac2 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -56,8 +56,8 @@ jobs: dotnet tool install --global dotnet-sonarscanner echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` - /k:"ppanchen_NetSdrClient" ` - /o:"ppanchen" ` + /k:"Dandddyy_ReengineeringCourse" ` + /o:"dandddyy" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` @@ -70,13 +70,13 @@ jobs: run: dotnet restore NetSdrClient.sln - name: Build run: dotnet build NetSdrClient.sln -c Release --no-restore - #- name: Tests with coverage (OpenCover) - # run: | - # dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` - # /p:CollectCoverage=true ` - # /p:CoverletOutput=TestResults/coverage.xml ` - # /p:CoverletOutputFormat=opencover - # shell: pwsh + - name: Tests with coverage (OpenCover) + run: | + dotnet test NetSdrClient.sln -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 0000000..f305dcd --- /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 : IDisposable + { + private readonly IPAddress _ipAddress; + private readonly int _port; + private TcpListener? _listener; + private CancellationTokenSource _cancellationTokenSource; + + public EchoServer(IPAddress ipAddress, int port) + { + _ipAddress = ipAddress; + _port = port; + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async Task StartAsync() + { + _listener = new TcpListener(_ipAddress, _port); + _listener.Start(); + Console.WriteLine($"Server started on {_ipAddress}:{_port}."); + + try + { + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + TcpClient client = await _listener.AcceptTcpClientAsync(_cancellationTokenSource.Token); + Console.WriteLine("Client connected."); + _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); + } + } + catch (OperationCanceledException) { /* Очікувано при вимкненні */ } + catch (Exception ex) { Console.WriteLine($"Server error: {ex.Message}"); } + finally { Console.WriteLine("Server shutdown."); } + } + + private async Task HandleClientAsync(TcpClient client, CancellationToken token) + { + using (client) + 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) + { + await stream.WriteAsync(buffer, 0, bytesRead, token); + Console.WriteLine($"Echoed {bytesRead} bytes to the client."); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Console.WriteLine($"Error handling client: {ex.Message}"); + } + } + } + + public void Stop() + { + if (!_cancellationTokenSource.IsCancellationRequested) + { + _cancellationTokenSource.Cancel(); + } + _listener?.Stop(); + } + + public void Dispose() + { + Stop(); + _cancellationTokenSource.Dispose(); + } + } +} \ No newline at end of file diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTcpServer/EchoServer.csproj index 2150e37..91b464a 100644 --- a/EchoTcpServer/EchoServer.csproj +++ b/EchoTcpServer/EchoServer.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/EchoTcpServer/IUdpClient.cs b/EchoTcpServer/IUdpClient.cs new file mode 100644 index 0000000..d21bc4e --- /dev/null +++ b/EchoTcpServer/IUdpClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Diagnostics.CodeAnalysis; + +namespace EchoTcpServer +{ + public interface IUdpClient : IDisposable + { + void Send(byte[] dgram, int bytes, IPEndPoint endPoint); + } + + [ExcludeFromCodeCoverage] + public class StandardUdpClient : IUdpClient + { + private readonly UdpClient _client = new UdpClient(); + public void Send(byte[] dgram, int bytes, IPEndPoint endPoint) => _client.Send(dgram, bytes, endPoint); + public void Dispose() => _client.Dispose(); + } +} \ No newline at end of file diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c57..011b208 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,173 +1,29 @@ using System; using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; -/// -/// 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) + [ExcludeFromCodeCoverage] + public class Program { - _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) + public static async Task Main(string[] args) { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); + using var server = new EchoServer(IPAddress.Any, 5000); + _ = Task.Run(() => server.StartAsync()); - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } + using var standardUdpClient = new StandardUdpClient(); + using var sender = new UdpTimedSender("127.0.0.1", 60000, standardUdpClient); - 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) - { - // 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); + sender.StartSending(5000); Console.WriteLine("Press 'q' to quit..."); - while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) - { - // Just wait until 'q' is pressed - } + while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { } 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 0000000..f5885ba --- /dev/null +++ b/EchoTcpServer/UdpTimedSender.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading; +using System.Security.Cryptography; + +namespace EchoTcpServer +{ + public class UdpTimedSender : IDisposable + { + private readonly string _host; + private readonly int _port; + private readonly IUdpClient _udpClient; + private Timer? _timer; + private ushort _messageCount = 0; + + public UdpTimedSender(string host, int port, IUdpClient udpClient) + { + _host = host; + _port = port; + _udpClient = udpClient; + } + + public void StartSending(int intervalMilliseconds) + { + if (_timer != null) + throw new InvalidOperationException("Sender is already running."); + + _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); + } + + private void SendMessageCallback(object? state) + { + try + { + byte[] samples = new byte[1024]; + RandomNumberGenerator.Fill(samples); + _messageCount++; + + byte[] msg = new byte[] { 0x04, 0x84 }.Concat(BitConverter.GetBytes(_messageCount)).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/EchoTcpServerTests/EchoServerTests.cs b/EchoTcpServerTests/EchoServerTests.cs new file mode 100644 index 0000000..21a6f22 --- /dev/null +++ b/EchoTcpServerTests/EchoServerTests.cs @@ -0,0 +1,58 @@ +using NUnit.Framework; +using EchoTcpServer; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoTcpServerTests +{ + [TestFixture] + public class EchoServerTests + { + [Test] + public async Task EchoServer_ShouldEchoDataBack() + { + // Arrange + int testPort = 5055; + using var server = new EchoServer(IPAddress.Loopback, testPort); + var serverTask = Task.Run(() => server.StartAsync()); + + await Task.Delay(100); + + using var client = new TcpClient(); + await client.ConnectAsync(IPAddress.Loopback, testPort); + using var stream = client.GetStream(); + + byte[] dataToSend = Encoding.UTF8.GetBytes("Hello Lab 6"); + byte[] receiveBuffer = new byte[1024]; + + // Act + await stream.WriteAsync(dataToSend, 0, dataToSend.Length); + int bytesRead = await stream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length); + string response = Encoding.UTF8.GetString(receiveBuffer, 0, bytesRead); + + // Assert + Assert.That(response, Is.EqualTo("Hello Lab 6")); + + // Cleanup + server.Stop(); + await serverTask; + } + + [Test] + public async Task Stop_ShouldGracefullyCancelListenerLoop() + { + // Arrange + using var server = new EchoServer(IPAddress.Loopback, 5056); + var serverTask = Task.Run(() => server.StartAsync()); + + await Task.Delay(50); + + // Act + server.Stop(); + + Assert.DoesNotThrowAsync(async () => await serverTask); + } + } +} \ No newline at end of file diff --git a/EchoTcpServerTests/EchoTcpServerTests.csproj b/EchoTcpServerTests/EchoTcpServerTests.csproj new file mode 100644 index 0000000..cc755d9 --- /dev/null +++ b/EchoTcpServerTests/EchoTcpServerTests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/EchoTcpServerTests/UdpTimedSenderTests.cs b/EchoTcpServerTests/UdpTimedSenderTests.cs new file mode 100644 index 0000000..441b41f --- /dev/null +++ b/EchoTcpServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,44 @@ +using Moq; +using NUnit.Framework; +using EchoTcpServer; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace EchoTcpServerTests +{ + [TestFixture] + public class UdpTimedSenderTests + { + [Test] + public void StartSending_WhenAlreadyRunning_ShouldThrowException() + { + var mockUdp = new Mock(); + using var sender = new UdpTimedSender("127.0.0.1", 60000, mockUdp.Object); + + sender.StartSending(1000); + + Assert.Throws(() => sender.StartSending(1000)); + } + + [Test] + public async Task StartSending_ShouldSendMessagesPeriodically() + { + // Arrange + var mockUdp = new Mock(); + int callCount = 0; + mockUdp.Setup(u => u.Send(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => callCount++); + + using var sender = new UdpTimedSender("127.0.0.1", 60000, mockUdp.Object); + + // Act + sender.StartSending(50); + await Task.Delay(150); + sender.StopSending(); + + // Assert + Assert.That(callCount, Is.GreaterThanOrEqualTo(2), "The timer should have triggered the sending at least 2 times."); + } + } +} \ No newline at end of file diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb..3b88600 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "Net EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoTcpServerTests", "EchoTcpServerTests\EchoTcpServerTests.csproj", "{7298C746-69F8-45A3-8A35-4513081C1936}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU + {7298C746-69F8-45A3-8A35-4513081C1936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7298C746-69F8-45A3-8A35-4513081C1936}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7298C746-69F8-45A3-8A35-4513081C1936}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7298C746-69F8-45A3-8A35-4513081C1936}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 0d69b4d..1fb0248 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -83,7 +83,7 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt msgEnumarable = msgEnumarable.Skip(_msgControlItemLength); msgLength -= _msgControlItemLength; - if (Enum.IsDefined(typeof(ControlItemCodes), value)) + if (Enum.IsDefined(typeof(ControlItemCodes), (int)value)) { itemCode = (ControlItemCodes)value; } diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c05..8d55e99 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,4 +1,4 @@ -using NetSdrClientApp.Messages; +using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; using System; using System.Collections.Generic; @@ -14,8 +14,8 @@ namespace NetSdrClientApp { public class NetSdrClient { - private ITcpClient _tcpClient; - private IUdpClient _udpClient; + private readonly ITcpClient _tcpClient; + private readonly IUdpClient _udpClient; public bool IQStarted { get; set; } @@ -66,7 +66,7 @@ public async Task StartIQAsync() return; } -; var iqDataMode = (byte)0x80; + var iqDataMode = (byte)0x80; var start = (byte)0x02; var fifo16bitCaptureMode = (byte)0x01; var n = (byte)1; @@ -116,7 +116,7 @@ public async Task ChangeFrequencyAsync(long hz, int channel) private void _udpClient_MessageReceived(object? sender, byte[] e) { - NetSdrMessageHelper.TranslateMessage(e, out MsgTypes type, out ControlItemCodes code, out ushort sequenceNum, out byte[] body); + NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body); var samples = NetSdrMessageHelper.GetSamples(16, body); Console.WriteLine($"Samples recieved: " + body.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); @@ -131,14 +131,14 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) } } - private TaskCompletionSource responseTaskSource; + private TaskCompletionSource? responseTaskSource; private async Task SendTcpRequest(byte[] msg) { if (!_tcpClient.Connected) { Console.WriteLine("No active connection."); - return null; + return Array.Empty(); } responseTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e..f18a547 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; @@ -87,15 +87,7 @@ public async Task SendMessageAsync(byte[] data) public async Task SendMessageAsync(string str) { var data = Encoding.UTF8.GetBytes(str); - if (Connected && _stream != null && _stream.CanWrite) - { - Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); - await _stream.WriteAsync(data, 0, data.Length); - } - else - { - throw new InvalidOperationException("Not connected to a server."); - } + await SendMessageAsync(data); } private async Task StartListeningAsync() diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b79..9ccc733 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; @@ -61,16 +61,7 @@ public void StopListening() public void Exit() { - try - { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) - { - Console.WriteLine($"Error while stopping: {ex.Message}"); - } + StopListening(); } public override int GetHashCode() @@ -82,4 +73,4 @@ public override int GetHashCode() return BitConverter.ToInt32(hash, 0); } -} \ No newline at end of file +} diff --git a/NetSdrClientAppTests/ArchitectureTests.cs b/NetSdrClientAppTests/ArchitectureTests.cs new file mode 100644 index 0000000..fa0da2d --- /dev/null +++ b/NetSdrClientAppTests/ArchitectureTests.cs @@ -0,0 +1,32 @@ +using NetArchTest.Rules; +using NetSdrClientApp; +using NUnit.Framework; + +namespace NetSdrClientAppTests +{ + [TestFixture] + public class ArchitectureTests + { + [Test] + public void Messages_ShouldNotDependOn_Networking() + { + var result = Types.InAssembly(typeof(NetSdrClient).Assembly) + .That().ResideInNamespace("NetSdrClientApp.Messages") + .ShouldNot().HaveDependencyOn("NetSdrClientApp.Networking") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, "Architectural error: The Messages module should not know about the Networking module!"); + } + + [Test] + public void Interfaces_ShouldStartWith_I() + { + var result = Types.InAssembly(typeof(NetSdrClient).Assembly) + .That().AreInterfaces() + .Should().HaveNameStartingWith("I") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True, "All interfaces must begin with the letter 'I'."); + } + } +} diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46a..470d4fa 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -10,9 +10,15 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + @@ -26,4 +32,4 @@ - + \ No newline at end of file diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f..562ae56 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -1,6 +1,8 @@ -using Moq; +using Moq; using NetSdrClientApp; using NetSdrClientApp.Networking; +using System.IO; +using System.Threading.Tasks; namespace NetSdrClientAppTests; @@ -35,6 +37,15 @@ public void Setup() _client = new NetSdrClient(_tcpMock.Object, _updMock.Object); } + + [TearDown] + public void TearDown() + { + if (File.Exists("samples.bin")) + { + File.Delete("samples.bin"); + } + } [Test] public async Task ConnectAsyncTest() @@ -61,7 +72,7 @@ public async Task DisconnectWithNoConnectionTest() [Test] public async Task DisconnectTest() { - //Arrange + //Arrange await ConnectAsyncTest(); //act @@ -75,7 +86,6 @@ public async Task DisconnectTest() [Test] public async Task StartIQNoConnectionTest() { - //act await _client.StartIQAsync(); @@ -88,7 +98,7 @@ public async Task StartIQNoConnectionTest() [Test] public async Task StartIQTest() { - //Arrange + //Arrange await ConnectAsyncTest(); //act @@ -103,7 +113,7 @@ public async Task StartIQTest() [Test] public async Task StopIQTest() { - //Arrange + //Arrange await ConnectAsyncTest(); //act @@ -115,5 +125,92 @@ public async Task StopIQTest() Assert.That(_client.IQStarted, Is.False); } - //TODO: cover the rest of the NetSdrClient code here + [Test] + public async Task ConnectAsync_WhenAlreadyConnected_ShouldNotSendMessages() + { + // Arrange + _tcpMock.Setup(tcp => tcp.Connected).Returns(true); + + // Act + await _client.ConnectAsync(); + + // Assert + _tcpMock.Verify(tcp => tcp.Connect(), Times.Never); + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task ChangeFrequencyAsync_WhenNoConnection_ShouldNotSendMessage() + { + // Arrange + _tcpMock.Setup(tcp => tcp.Connected).Returns(false); + + // Act + await _client.ChangeFrequencyAsync(14000000, 0); + + // Assert + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task ChangeFrequencyAsync_WhenConnected_ShouldSendMessage() + { + // Arrange + await _client.ConnectAsync(); + + // Act + await _client.ChangeFrequencyAsync(14000000, 0); + + // Assert + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); + } + + [Test] + public void TcpClient_MessageReceived_Unsolicited_DoesNotThrow() + { + // Arrange + byte[] dummyMessage = { 0x00, 0x01, 0x02 }; + + // Act & Assert + Assert.DoesNotThrow(() => + { + _tcpMock.Raise(tcp => tcp.MessageReceived += null, this, dummyMessage); + }); + } + + [Test] + public void NetSdrClient_InitialState_IQStartedIsFalse() + { + Assert.That(_client.IQStarted, Is.False); + } + + [Test] + public async Task StopIQNoConnectionTest() + { + // Act + await _client.StopIQAsync(); + + // Assert + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + _tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce); + } + + [Test] + public void UdpClient_MessageReceived_ShouldWriteToFile() + { + // Arrange + byte[] dummyPacket = new byte[64]; + + dummyPacket[0] = 64; + dummyPacket[1] = 0; + + // Act + Assert.DoesNotThrow(() => + { + _updMock.Raise(udp => udp.MessageReceived += null, this, dummyPacket); + }); + + // Assert + Assert.That(File.Exists("samples.bin"), Is.True, "Файл samples.bin має бути створений"); + } } diff --git a/NetSdrClientAppTests/TcpClientWrapperTests.cs b/NetSdrClientAppTests/TcpClientWrapperTests.cs new file mode 100644 index 0000000..a78a854 --- /dev/null +++ b/NetSdrClientAppTests/TcpClientWrapperTests.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using NetSdrClientApp.Networking; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace NetSdrClientAppTests +{ + [TestFixture] + public class TcpClientWrapperTests + { + [Test] + public void Connected_InitialState_ShouldBeFalse() + { + var wrapper = new TcpClientWrapper("127.0.0.1", 12345); + Assert.That(wrapper.Connected, Is.False); + } + + [Test] + public void Disconnect_WhenNotConnected_ShouldNotThrow() + { + var wrapper = new TcpClientWrapper("127.0.0.1", 12345); + Assert.DoesNotThrow(() => wrapper.Disconnect()); + } + + [Test] + public void SendMessageAsync_Bytes_WhenNotConnected_ShouldThrowInvalidOperationException() + { + var wrapper = new TcpClientWrapper("127.0.0.1", 12345); + Assert.ThrowsAsync(async () => await wrapper.SendMessageAsync(new byte[] { 1, 2, 3 })); + } + + [Test] + public void SendMessageAsync_String_WhenNotConnected_ShouldThrowInvalidOperationException() + { + var wrapper = new TcpClientWrapper("127.0.0.1", 12345); + Assert.ThrowsAsync(async () => await wrapper.SendMessageAsync("Hello")); + } + + [Test] + public void Connect_WhenServerIsDown_ShouldCatchExceptionAndRemainDisconnected() + { + var wrapper = new TcpClientWrapper("127.0.0.1", 55555); + wrapper.Connect(); + + Assert.That(wrapper.Connected, Is.False); + } + + [Test] + public async Task ConnectAndDisconnect_WithRealLocalServer_ShouldChangeState() + { + // Arrange + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var wrapper = new TcpClientWrapper("127.0.0.1", port); + + // Act + wrapper.Connect(); + + using var client = await listener.AcceptTcpClientAsync(); + + // Assert + Assert.That(wrapper.Connected, Is.True); + + // Act + wrapper.Disconnect(); + + // Assert + Assert.That(wrapper.Connected, Is.False); + + listener.Stop(); + } + } +} diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs new file mode 100644 index 0000000..17e8f9e --- /dev/null +++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using NetSdrClientApp.Networking; +using System.Threading.Tasks; + +namespace NetSdrClientAppTests +{ + [TestFixture] + public class UdpClientWrapperTests + { + [Test] + public void StopListening_WhenNotStarted_ShouldNotThrow() + { + var wrapper = new UdpClientWrapper(0); + Assert.DoesNotThrow(() => wrapper.StopListening()); + } + + [Test] + public void Exit_WhenNotStarted_ShouldNotThrow() + { + var wrapper = new UdpClientWrapper(0); + Assert.DoesNotThrow(() => wrapper.Exit()); + } + + [Test] + public void GetHashCode_ShouldReturnConsistentValueForSameEndpoint() + { + // Arrange + var wrapper1 = new UdpClientWrapper(12348); + var wrapper2 = new UdpClientWrapper(12348); + + // Act & Assert + Assert.That(wrapper1.GetHashCode(), Is.EqualTo(wrapper2.GetHashCode())); + } + + [Test] + public async Task StartAndStopListening_ShouldHandleCancellationGracefully() + { + // Arrange + var wrapper = new UdpClientWrapper(0); + + // Act + var listenTask = wrapper.StartListeningAsync(); + + await Task.Delay(50); + + wrapper.StopListening(); + + // Assert + Assert.DoesNotThrowAsync(async () => await listenTask); + } + } +} diff --git a/README.md b/README.md index b3a9029..5da61fc 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # Лабораторні з реінжинірингу (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) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=bugs)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=bugs)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Dandddyy_ReengineeringCourse&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=Dandddyy_ReengineeringCourse) Цей репозиторій використовується для курсу **реінжиніринг ПЗ**.