diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e784069..49217c6 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -37,6 +37,8 @@ on: permissions: pull-requests: read # allows SonarCloud to decorate PRs with analysis results + contents: read + jobs: sonar-check: @@ -56,8 +58,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:"slavik22_ReengineeringCourse" ` + /o:"slavik22" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` @@ -70,13 +72,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 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 0000000..e1288f5 --- /dev/null +++ b/EchoTcpServer/EchoServer.cs @@ -0,0 +1,71 @@ +using EchoTcpServer.Networking; + +namespace EchoTcpServer; + +public class EchoServer +{ + private readonly ITcpListener _listener; + private readonly Action _log; + private CancellationTokenSource _cts = new(); + + public EchoServer(ITcpListener listener, Action? log = null) + { + _listener = listener; + _log = log ?? Console.WriteLine; + } + + public async Task StartAsync() + { + _listener.Start(); + _log("Server started."); + + while (!_cts.Token.IsCancellationRequested) + { + try + { + var stream = await _listener.AcceptClientStreamAsync(); + _log("Client connected."); + _ = Task.Run(() => HandleClientAsync(stream, _cts.Token)); + } + catch (ObjectDisposedException) + { + break; + } + } + + _log("Server shutdown."); + } + + public async Task HandleClientAsync(Stream stream, CancellationToken token) + { + using (stream) + { + 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); + _log($"Echoed {bytesRead} bytes."); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _log($"Error: {ex.Message}"); + } + } + + _log("Client disconnected."); + } + + public void Stop() + { + _cts.Cancel(); + _listener.Stop(); + _cts.Dispose(); + _log("Server stopped."); + } +} diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTcpServer/EchoServer.csproj index 2150e37..46e738e 100644 --- a/EchoTcpServer/EchoServer.csproj +++ b/EchoTcpServer/EchoServer.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + EchoTcpServer diff --git a/EchoTcpServer/Networking/ITcpListener.cs b/EchoTcpServer/Networking/ITcpListener.cs new file mode 100644 index 0000000..9952cc0 --- /dev/null +++ b/EchoTcpServer/Networking/ITcpListener.cs @@ -0,0 +1,8 @@ +namespace EchoTcpServer.Networking; + +public interface ITcpListener +{ + void Start(); + void Stop(); + Task AcceptClientStreamAsync(); +} diff --git a/EchoTcpServer/Networking/TcpListenerWrapper.cs b/EchoTcpServer/Networking/TcpListenerWrapper.cs new file mode 100644 index 0000000..67b84a7 --- /dev/null +++ b/EchoTcpServer/Networking/TcpListenerWrapper.cs @@ -0,0 +1,23 @@ +using System.Net; +using System.Net.Sockets; + +namespace EchoTcpServer.Networking; + +public class TcpListenerWrapper : ITcpListener +{ + private readonly TcpListener _listener; + + public TcpListenerWrapper(int port) + { + _listener = new TcpListener(IPAddress.Any, port); + } + + public void Start() => _listener.Start(); + public void Stop() => _listener.Stop(); + + public async Task AcceptClientStreamAsync() + { + var client = await _listener.AcceptTcpClientAsync(); + return client.GetStream(); + } +} diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c57..ab9890f 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,173 +1,16 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using EchoTcpServer; +using EchoTcpServer.Networking; -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer -{ - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; +var server = new EchoServer(new TcpListenerWrapper(5000)); +_ = Task.Run(() => server.StartAsync()); +using var sender = new UdpTimedSender("127.0.0.1", 60000); +Console.WriteLine("Press any key to stop sending..."); +sender.StartSending(5000); - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } +Console.WriteLine("Press 'q' to quit..."); +while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { } - public async Task StartAsync() - { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); - - _ = 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) - { - // 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(); - 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 +sender.StopSending(); +server.Stop(); +Console.WriteLine("Sender stopped."); diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs new file mode 100644 index 0000000..aabf3a6 --- /dev/null +++ b/EchoTcpServer/UdpTimedSender.cs @@ -0,0 +1,64 @@ +using System.Net; +using System.Net.Sockets; + +namespace EchoTcpServer; + +public class UdpTimedSender : IDisposable +{ + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private readonly Random _rnd = new(); + private Timer? _timer; + private ushort _sequenceNumber; + + 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); + } + + private void SendMessageCallback(object? state) + { + try + { + byte[] samples = new byte[1024]; + _rnd.NextBytes(samples); + _sequenceNumber++; + + byte[] msg = new byte[] { 0x04, 0x84 } + .Concat(BitConverter.GetBytes(_sequenceNumber)) + .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(); + } +} diff --git a/EchoTcpServerTests/EchoServerTests.cs b/EchoTcpServerTests/EchoServerTests.cs new file mode 100644 index 0000000..b81c47f --- /dev/null +++ b/EchoTcpServerTests/EchoServerTests.cs @@ -0,0 +1,159 @@ +using Moq; +using EchoTcpServer; +using EchoTcpServer.Networking; + +namespace EchoTcpServerTests; + +[TestFixture] +public class EchoServerTests +{ + private Mock _listenerMock = null!; + private List _logs = null!; + private EchoServer _server = null!; + + [SetUp] + public void SetUp() + { + _listenerMock = new Mock(); + _logs = new List(); + _server = new EchoServer(_listenerMock.Object, msg => _logs.Add(msg)); + } + + [Test] + public void Stop_StopsListenerAndLogs() + { + _server.Stop(); + + _listenerMock.Verify(l => l.Stop(), Times.Once); + Assert.That(_logs, Contains.Item("Server stopped.")); + } + + [Test] + public async Task StartAsync_StartsListenerAndLogsShutdown() + { + _listenerMock + .Setup(l => l.AcceptClientStreamAsync()) + .ThrowsAsync(new ObjectDisposedException("listener")); + + await _server.StartAsync(); + + _listenerMock.Verify(l => l.Start(), Times.Once); + Assert.That(_logs, Contains.Item("Server started.")); + Assert.That(_logs, Contains.Item("Server shutdown.")); + } + + [Test] + public async Task StartAsync_AcceptsClientAndLogs() + { + _listenerMock.SetupSequence(l => l.AcceptClientStreamAsync()) + .ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })) + .ThrowsAsync(new ObjectDisposedException("listener")); + + await _server.StartAsync(); + + _listenerMock.Verify(l => l.AcceptClientStreamAsync(), Times.Exactly(2)); + Assert.That(_logs, Contains.Item("Client connected.")); + } + + [Test] + public async Task HandleClientAsync_EchoesInputToOutput() + { + var inputData = new byte[] { 0x41, 0x42, 0x43 }; // "ABC" + var inputStream = new MemoryStream(inputData); + var outputStream = new MemoryStream(); + var stream = new DuplexStream(inputStream, outputStream); + + await _server.HandleClientAsync(stream, CancellationToken.None); + + Assert.That(outputStream.ToArray(), Is.EqualTo(inputData)); + } + + [Test] + public async Task HandleClientAsync_LogsEchoedBytes() + { + var stream = new DuplexStream(new MemoryStream(new byte[] { 1, 2 }), new MemoryStream()); + + await _server.HandleClientAsync(stream, CancellationToken.None); + + Assert.That(_logs.Any(m => m.StartsWith("Echoed")), Is.True); + Assert.That(_logs, Contains.Item("Client disconnected.")); + } + + [Test] + public async Task HandleClientAsync_PreCancelledToken_DoesNotRead() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var mockStream = new Mock(); + mockStream.Setup(s => s.CanRead).Returns(true); + + await _server.HandleClientAsync(mockStream.Object, cts.Token); + + mockStream.Verify( + s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleClientAsync_StreamException_LogsError() + { + var mockStream = new Mock(); + mockStream + .Setup(s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new IOException("Connection reset by peer")); + + await _server.HandleClientAsync(mockStream.Object, CancellationToken.None); + + Assert.That(_logs.Any(m => m.StartsWith("Error:")), Is.True); + } + + [Test] + public async Task HandleClientAsync_EmptyStream_LogsDisconnected() + { + var emptyStream = new DuplexStream(new MemoryStream(Array.Empty()), new MemoryStream()); + + await _server.HandleClientAsync(emptyStream, CancellationToken.None); + + Assert.That(_logs, Contains.Item("Client disconnected.")); + } +} + +/// +/// Reads from one stream, writes to another — simulates a bidirectional client connection. +/// +internal sealed class DuplexStream : Stream +{ + private readonly Stream _read; + private readonly Stream _write; + + public DuplexStream(Stream read, Stream write) + { + _read = read; + _write = write; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _write.Flush(); + public override int Read(byte[] buffer, int offset, int count) => _read.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _read.ReadAsync(buffer, offset, count, ct); + + public override void Write(byte[] buffer, int offset, int count) => _write.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _write.WriteAsync(buffer, offset, count, ct); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); +} diff --git a/EchoTcpServerTests/EchoTcpServerTests.csproj b/EchoTcpServerTests/EchoTcpServerTests.csproj new file mode 100644 index 0000000..b46e32b --- /dev/null +++ b/EchoTcpServerTests/EchoTcpServerTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/EchoTcpServerTests/UdpTimedSenderTests.cs b/EchoTcpServerTests/UdpTimedSenderTests.cs new file mode 100644 index 0000000..616b8b7 --- /dev/null +++ b/EchoTcpServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,51 @@ +using EchoTcpServer; + +namespace EchoTcpServerTests; + +[TestFixture] +public class UdpTimedSenderTests +{ + [Test] + public void StartSending_WhenAlreadyRunning_ThrowsInvalidOperationException() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.Throws(() => sender.StartSending(60_000)); + } + + [Test] + public void StopSending_WithoutStarting_DoesNotThrow() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.StopSending()); + } + + [Test] + public void StopSending_AfterStart_AllowsSecondStart() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + sender.StopSending(); + + Assert.DoesNotThrow(() => sender.StartSending(60_000)); + } + + [Test] + public void Dispose_WithoutStarting_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.Dispose()); + } + + [Test] + public void Dispose_WhileSending_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.DoesNotThrow(() => sender.Dispose()); + } +} diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb..959be4e 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,24 +9,68 @@ 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", "{E6BC0BE9-A629-48C5-A08A-8373A21434F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EchoTcpServer", "EchoTcpServer", "{DBDFFC83-AD19-3622-0FF4-C4288E16DE63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x64.Build.0 = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x86.Build.0 = Debug|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|Any CPU.Build.0 = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x64.ActiveCfg = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x64.Build.0 = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x86.ActiveCfg = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x86.Build.0 = Release|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x64.Build.0 = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x86.Build.0 = Debug|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|Any CPU.Build.0 = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x64.ActiveCfg = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x64.Build.0 = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x86.ActiveCfg = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x86.Build.0 = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x64.ActiveCfg = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x64.Build.0 = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x86.ActiveCfg = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x86.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 + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x64.ActiveCfg = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x64.Build.0 = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x86.ActiveCfg = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x86.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x64.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x86.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|Any CPU.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x64.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x64.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x86.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x86.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..b0ec931 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.PortableExecutable; -using System.Text; -using System.Threading.Tasks; - + namespace NetSdrClientApp.Messages { //TODO: analyze possible use of [StructLayout] for better performance and readability @@ -83,7 +77,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; } @@ -111,7 +105,7 @@ public static IEnumerable GetSamples(ushort sampleSize, byte[] body) sampleSize /= 8; //to bytes if (sampleSize > 4) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(sampleSize), sampleSize, "Sample size must be 8, 16, 24, or 32 bits."); } var bodyEnumerable = body as IEnumerable; diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c05..1b9a04f 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,21 +1,13 @@ using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; using static NetSdrClientApp.Messages.NetSdrMessageHelper; -using static System.Runtime.InteropServices.JavaScript.JSType; 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 +58,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 +108,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,9 +123,9 @@ private void _udpClient_MessageReceived(object? sender, byte[] e) } } - private TaskCompletionSource responseTaskSource; + private TaskCompletionSource? responseTaskSource; - private async Task SendTcpRequest(byte[] msg) + private async Task SendTcpRequest(byte[] msg) { if (!_tcpClient.Connected) { diff --git a/NetSdrClientApp/Networking/ITcpClient.cs b/NetSdrClientApp/Networking/ITcpClient.cs index 3470b5d..e779e95 100644 --- a/NetSdrClientApp/Networking/ITcpClient.cs +++ b/NetSdrClientApp/Networking/ITcpClient.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace NetSdrClientApp.Networking +namespace NetSdrClientApp.Networking { public interface ITcpClient { diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs index 1b9f931..053262b 100644 --- a/NetSdrClientApp/Networking/IUdpClient.cs +++ b/NetSdrClientApp/Networking/IUdpClient.cs @@ -1,10 +1,13 @@ - -public interface IUdpClient + +namespace NetSdrClientApp.Networking { - event EventHandler? MessageReceived; + public interface IUdpClient + { + event EventHandler? MessageReceived; - Task StartListeningAsync(); + Task StartListeningAsync(); - void StopListening(); - void Exit(); -} \ No newline at end of file + void StopListening(); + void Exit(); + } +} diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e..ade0102 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -1,22 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Sockets; +using System.Net.Sockets; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace NetSdrClientApp.Networking { public class TcpClientWrapper : ITcpClient { - private string _host; - private int _port; + private readonly string _host; + private readonly int _port; private TcpClient? _tcpClient; private NetworkStream? _stream; - private CancellationTokenSource _cts; + private CancellationTokenSource? _cts; public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null; @@ -57,6 +50,7 @@ public void Disconnect() if (Connected) { _cts?.Cancel(); + _cts?.Dispose(); _stream?.Close(); _tcpClient?.Close(); @@ -71,22 +65,12 @@ public void Disconnect() } } - public async Task SendMessageAsync(byte[] data) - { - 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."); - } - } + public Task SendMessageAsync(byte[] data) => SendBytesAsync(data); + + public Task SendMessageAsync(string str) => SendBytesAsync(Encoding.UTF8.GetBytes(str)); - public async Task SendMessageAsync(string str) + private async Task SendBytesAsync(byte[] data) { - 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}")); @@ -117,7 +101,7 @@ private async Task StartListeningAsync() } } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { //empty } diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b79..8201c46 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,85 +1,73 @@ -using System; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; -using System.Threading; -using System.Threading.Tasks; -public class UdpClientWrapper : IUdpClient +namespace NetSdrClientApp.Networking { - private readonly IPEndPoint _localEndPoint; - private CancellationTokenSource? _cts; - private UdpClient? _udpClient; - - public event EventHandler? MessageReceived; - - public UdpClientWrapper(int port) + public class UdpClientWrapper : IUdpClient { - _localEndPoint = new IPEndPoint(IPAddress.Any, port); - } + private readonly IPEndPoint _localEndPoint; + private CancellationTokenSource? _cts; + private UdpClient? _udpClient; - public async Task StartListeningAsync() - { - _cts = new CancellationTokenSource(); - Console.WriteLine("Start listening for UDP messages..."); + public event EventHandler? MessageReceived; - try + public UdpClientWrapper(int port) { - _udpClient = new UdpClient(_localEndPoint); - while (!_cts.Token.IsCancellationRequested) + _localEndPoint = new IPEndPoint(IPAddress.Any, port); + } + + public async Task StartListeningAsync() + { + _cts = new CancellationTokenSource(); + Console.WriteLine("Start listening for UDP messages..."); + + try { - UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); - MessageReceived?.Invoke(this, result.Buffer); + _udpClient = new UdpClient(_localEndPoint); + while (!_cts.Token.IsCancellationRequested) + { + UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); + MessageReceived?.Invoke(this, result.Buffer); - Console.WriteLine($"Received from {result.RemoteEndPoint}"); + Console.WriteLine($"Received from {result.RemoteEndPoint}"); + } + } + catch (OperationCanceledException) + { + //empty + } + catch (Exception ex) + { + Console.WriteLine($"Error receiving message: {ex.Message}"); } } - catch (OperationCanceledException ex) - { - //empty - } - catch (Exception ex) - { - Console.WriteLine($"Error receiving message: {ex.Message}"); - } - } - public void StopListening() - { - try - { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) + public void StopListening() { - Console.WriteLine($"Error while stopping: {ex.Message}"); + try + { + _cts?.Cancel(); + _cts?.Dispose(); + _udpClient?.Close(); + Console.WriteLine("Stopped listening for UDP messages."); + } + catch (Exception ex) + { + Console.WriteLine($"Error while stopping: {ex.Message}"); + } } - } - 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}"); - } - } + public void Exit() => StopListening(); - public override int GetHashCode() - { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; + 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)); + var hash = MD5.HashData(Encoding.UTF8.GetBytes(payload)); - return BitConverter.ToInt32(hash, 0); + return BitConverter.ToInt32(hash, 0); + } } -} \ No newline at end of file +} diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46a..eb99533 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -11,6 +11,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index ad00c4f..b52e77e 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -115,5 +115,29 @@ public async Task StopIQTest() Assert.That(_client.IQStarted, Is.False); } + [Test] + public async Task StopIQNoConnectionTest() + { + // act — no ConnectAsync, so Connected is false + await _client.StopIQAsync(); + + // assert + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); + _tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce); + } + + [Test] + public async Task ChangeFrequencyAsyncTest() + { + // arrange + await ConnectAsyncTest(); // 3 setup messages + + // act + await _client.ChangeFrequencyAsync(20_000_000, 1); + + // assert — 4th SendMessageAsync for frequency change + _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(4)); + } + //TODO: cover the rest of the NetSdrClient code here } diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index b40fff7..285b694 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -64,6 +64,46 @@ public void GetDataItemMessageTest() Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); } + [Test] + public void TranslateMessageRoundTripTest() + { + // arrange — build a SetControlItem message then parse it back + var parameters = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + var msg = NetSdrMessageHelper.GetControlItemMessage( + NetSdrMessageHelper.MsgTypes.SetControlItem, + NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency, + parameters); + + // act + bool success = NetSdrMessageHelper.TranslateMessage(msg, + out var type, out var itemCode, out _, out var body); + + // assert + Assert.That(success, Is.True); + Assert.That(type, Is.EqualTo(NetSdrMessageHelper.MsgTypes.SetControlItem)); + Assert.That(itemCode, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.ReceiverFrequency)); + Assert.That(body, Is.EqualTo(parameters)); + } + + [Test] + public void GetSamples16BitReturnsCorrectCountTest() + { + // 6 bytes → 3 samples of 16 bits + var body = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + + var samples = NetSdrMessageHelper.GetSamples(16, body).ToList(); + + Assert.That(samples.Count, Is.EqualTo(3)); + } + + [Test] + public void GetSamplesThrowsForOversizedBitsTest() + { + // 40 bits / 8 = 5 bytes > 4 → ArgumentOutOfRangeException + Assert.Throws(() => + NetSdrMessageHelper.GetSamples(40, new byte[8]).ToList()); + } + //TODO: add more NetSdrMessageHelper tests } } \ No newline at end of file