From 0e006235af157be14d7251892866bef107849a98 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 23 Oct 2025 16:57:44 +0300 Subject: [PATCH 01/28] =?UTF-8?q?=D0=97=D0=BC=D0=B5=D0=BD=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8F=20=D0=B4=D1=83=D0=B1=D0=BB=D1=96=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BA=D0=BE=D0=B4=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Networking/TcpClientWrapper.cs | 17 +++---- .../Networking/UdpClientWrapper.cs | 46 +++++++++++-------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e..d23aa50 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -73,20 +73,17 @@ 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."); - } + await SendMessageInternalAsync(data); } public async Task SendMessageAsync(string str) { - var data = Encoding.UTF8.GetBytes(str); + await SendMessageInternalAsync(Encoding.UTF8.GetBytes(str)); + + } + + private async Task SendMessageInternalAsync(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}")); diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b79..91bb71e 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -47,30 +47,12 @@ public async Task StartListeningAsync() public void StopListening() { - try - { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) - { - Console.WriteLine($"Error while stopping: {ex.Message}"); - } + StopInternal(); } 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}"); - } + StopInternal(); } public override int GetHashCode() @@ -82,4 +64,28 @@ public override int GetHashCode() return BitConverter.ToInt32(hash, 0); } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) return true; + if (obj is not UdpClientWrapper other) return false; + + + return _localEndPoint.Address.Equals(other._localEndPoint.Address) + && _localEndPoint.Port == other._localEndPoint.Port; + } + + private void StopInternal() + { + try + { + _cts?.Cancel(); + _udpClient?.Close(); + Console.WriteLine("Stopped listening for UDP messages."); + } + catch (Exception ex) + { + Console.WriteLine($"Error while stopping: {ex.Message}"); + } + } } \ No newline at end of file From f2759b87fcd2cd17054cb9ca45a9e5a915878421 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 23 Oct 2025 16:59:59 +0300 Subject: [PATCH 02/28] Update SonarCloud badges for Missile2006 project --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0e05e91..7e1e774 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Лабораторні з реінжинірингу (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=Missile2006_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Missile2006_NetSdrClient) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Missile2006_NetSdrClient&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Missile2006_NetSdrClient) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=Missile2006_NetSdrClient&metric=bugs)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=Missile2006_NetSdrClient&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=Missile2006_NetSdrClient) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Missile2006_NetSdrClient&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=Missile2006_NetSdrClient) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Missile2006_NetSdrClient&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Missile2006_NetSdrClient) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Missile2006_NetSdrClient&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=Missile2006_NetSdrClient) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Missile2006_NetSdrClient&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=Missile2006_NetSdrClient) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=chainmeJB_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=chainmeJB_NetSdrClient) [![Quality gate](https://sonarcloud.io/api/project_badges/quality_gate?project=chainmeJB_NetSdrClient)](https://sonarcloud.io/summary/new_code?id=chainmeJB_NetSdrClient) From 65450e468a4b811de055b692e3916d803bf514f8 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 23 Oct 2025 17:26:02 +0300 Subject: [PATCH 03/28] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Messages/NetSdrMessageHelper.cs | 1 + NetSdrClientAppTests/NetSdrClientTests.cs | 96 +++++++------------ .../NetSdrMessageHelperTests.cs | 36 ++++++- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index f718353..19e9f54 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -162,5 +162,6 @@ private static void TranslateHeader(byte[] header, out MsgTypes type, out int ms msgLength = _maxDataItemMessageLength; } } + } } diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index e8915bf..c0d9c58 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -4,13 +4,12 @@ namespace NetSdrClientAppTests; +[TestFixture] public class NetSdrClientTests { - NetSdrClient _client; - Mock _tcpMock; - Mock _updMock; - - public NetSdrClientTests() { } + private NetSdrClient _client = null!; + private Mock _tcpMock = null!; + private Mock _udpMock = null!; [SetUp] public void Setup() @@ -26,61 +25,54 @@ public void Setup() _tcpMock.Setup(tcp => tcp.Connected).Returns(false); }); - _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())).Callback((bytes) => - { - _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes); - }); + _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())) + .Returns(Task.CompletedTask); - _updMock = new Mock(); + _udpMock = new Mock(); + _udpMock.Setup(udp => udp.StartListeningAsync()) + .Returns(Task.CompletedTask); - _client = new NetSdrClient(_tcpMock.Object, _updMock.Object); + _client = new NetSdrClient(_tcpMock.Object, _udpMock.Object); } [Test] public async Task ConnectAsyncTest() { - //act + // Act await _client.ConnectAsync(); - //assert + // Assert _tcpMock.Verify(tcp => tcp.Connect(), Times.Once); _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(3)); } [Test] - public async Task DisconnectWithNoConnectionTest() + public void DisconnectWithNoConnectionTest() { - //act - _client.Disconect(); - - //assert - //No exception thrown + // Act & Assert + Assert.DoesNotThrow(() => _client.Disconect()); _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } [Test] public async Task DisconnectTest() { - //Arrange - await ConnectAsyncTest(); + // Arrange + await _client.ConnectAsync(); - //act + // Act _client.Disconect(); - //assert - //No exception thrown + // Assert _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } [Test] public async Task StartIQNoConnectionTest() { - - //act + // Act & Assert await _client.StartIQAsync(); - //assert - //No exception thrown _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); _tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce); } @@ -88,30 +80,29 @@ public async Task StartIQNoConnectionTest() [Test] public async Task StartIQTest() { - //Arrange - await ConnectAsyncTest(); + // Arrange + await _client.ConnectAsync(); - //act + // Act await _client.StartIQAsync(); - //assert - //No exception thrown - _updMock.Verify(udp => udp.StartListeningAsync(), Times.Once); + // Assert + _udpMock.Verify(udp => udp.StartListeningAsync(), Times.Once); Assert.That(_client.IQStarted, Is.True); } [Test] public async Task StopIQTest() { - //Arrange - await ConnectAsyncTest(); + // Arrange + await _client.ConnectAsync(); + await _client.StartIQAsync(); - //act + // Act await _client.StopIQAsync(); - //assert - //No exception thrown - _updMock.Verify(tcp => tcp.StopListening(), Times.Once); + // Assert + _udpMock.Verify(udp => udp.StopListening(), Times.Once); Assert.That(_client.IQStarted, Is.False); } @@ -122,32 +113,9 @@ public async Task ChangeFrequencyAsync_SendsMessage() await _client.ConnectAsync(); // Act - await _client.ChangeFrequencyAsync(144000000, 1); // 144 MHz, channel 1 + await _client.ChangeFrequencyAsync(144000000, 1); // Assert _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.AtLeast(4)); } - - [Test] - public void TcpClient_MessageReceived_SetsResponseTask() - { - // Arrange - var bytes = new byte[] { 0x01, 0x02, 0x03 }; - var tcpClientField = typeof(NetSdrClient) - .GetField("responseTaskSource", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - var tcs = new TaskCompletionSource(); - tcpClientField.SetValue(_client, tcs); - - // Act - var method = typeof(NetSdrClient) - .GetMethod("_tcpClient_MessageReceived", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - method.Invoke(_client, new object?[] { null, bytes }); - - // Assert - Assert.IsTrue(tcs.Task.IsCompleted); - Assert.That(tcs.Task.Result, Is.EqualTo(bytes)); - } - //TODO: cover the rest of the NetSdrClient code here } \ No newline at end of file diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index b40fff7..2440537 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -63,7 +63,41 @@ public void GetDataItemMessageTest() Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); } + [Test] + public void GetControlItemMessage_WithNullParameters_ThrowsException() + { + // Arrange + var type = NetSdrMessageHelper.MsgTypes.Ack; + var code = NetSdrMessageHelper.ControlItemCodes.ReceiverState; + + // Act & Assert + Assert.Throws(() => + NetSdrMessageHelper.GetControlItemMessage(type, code, null!)); + } + + [Test] + public void GetDataItemMessage_WithNullParameters_ThrowsException() + { + // Arrange + var type = NetSdrMessageHelper.MsgTypes.DataItem2; - //TODO: add more NetSdrMessageHelper tests + // Act & Assert + Assert.Throws(() => + NetSdrMessageHelper.GetDataItemMessage(type, null!)); + } + + [Test] + public void GetControlItemMessage_WithEmptyParameters() + { + // Arrange + var type = NetSdrMessageHelper.MsgTypes.Ack; + var code = NetSdrMessageHelper.ControlItemCodes.ReceiverState; + + // Act + byte[] msg = NetSdrMessageHelper.GetControlItemMessage(type, code, Array.Empty()); + + // Assert + Assert.That(msg.Length, Is.EqualTo(4)); // Only header + code + } } } \ No newline at end of file From d471233725635c3a9ebbfe74f1a33d64f7b3bf67 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 23 Oct 2025 17:48:47 +0300 Subject: [PATCH 04/28] Update sonarcloud.yml --- .github/workflows/sonarcloud.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 02ba1c6..593c797 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -49,21 +49,22 @@ jobs: - uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - + + - name: Install SonarScanner + run: | + dotnet tool install --global dotnet-sonarscanner + echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH # 1) BEGIN: SonarScanner for .NET # 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:"Missile2006_NetSdrClient" ` /o:"missile2006" ` /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.coverage.exclusions="**Test*.cs" ` + /d:sonar.exclusions="**/bin/**,**/obj/**,**/sonarcloud.yml" ` /d:sonar.qualitygate.wait=true shell: pwsh From 5f74d406035db7d5e5be4c2b8a26d24d7861f72a Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 23 Oct 2025 17:54:19 +0300 Subject: [PATCH 05/28] Update sonarcloud.yml --- .github/workflows/sonarcloud.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 593c797..02ba1c6 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -49,22 +49,21 @@ jobs: - uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - - - name: Install SonarScanner - run: | - dotnet tool install --global dotnet-sonarscanner - echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH + # 1) BEGIN: SonarScanner for .NET # 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:"Missile2006_NetSdrClient" ` /o:"missile2006" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` - /d:sonar.coverage.exclusions="**Test*.cs" ` - /d:sonar.exclusions="**/bin/**,**/obj/**,**/sonarcloud.yml" ` + /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 From 67731d224300a60655e1395cfd5c90a1635434c8 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 23 Oct 2025 17:55:24 +0300 Subject: [PATCH 06/28] . --- NetSdrClientAppTests/NetSdrClientTests.cs | 96 ++++++++++++------- .../NetSdrMessageHelperTests.cs | 35 ------- 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index c0d9c58..e8915bf 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -4,12 +4,13 @@ namespace NetSdrClientAppTests; -[TestFixture] public class NetSdrClientTests { - private NetSdrClient _client = null!; - private Mock _tcpMock = null!; - private Mock _udpMock = null!; + NetSdrClient _client; + Mock _tcpMock; + Mock _updMock; + + public NetSdrClientTests() { } [SetUp] public void Setup() @@ -25,54 +26,61 @@ public void Setup() _tcpMock.Setup(tcp => tcp.Connected).Returns(false); }); - _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())) - .Returns(Task.CompletedTask); + _tcpMock.Setup(tcp => tcp.SendMessageAsync(It.IsAny())).Callback((bytes) => + { + _tcpMock.Raise(tcp => tcp.MessageReceived += null, _tcpMock.Object, bytes); + }); - _udpMock = new Mock(); - _udpMock.Setup(udp => udp.StartListeningAsync()) - .Returns(Task.CompletedTask); + _updMock = new Mock(); - _client = new NetSdrClient(_tcpMock.Object, _udpMock.Object); + _client = new NetSdrClient(_tcpMock.Object, _updMock.Object); } [Test] public async Task ConnectAsyncTest() { - // Act + //act await _client.ConnectAsync(); - // Assert + //assert _tcpMock.Verify(tcp => tcp.Connect(), Times.Once); _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Exactly(3)); } [Test] - public void DisconnectWithNoConnectionTest() + public async Task DisconnectWithNoConnectionTest() { - // Act & Assert - Assert.DoesNotThrow(() => _client.Disconect()); + //act + _client.Disconect(); + + //assert + //No exception thrown _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } [Test] public async Task DisconnectTest() { - // Arrange - await _client.ConnectAsync(); + //Arrange + await ConnectAsyncTest(); - // Act + //act _client.Disconect(); - // Assert + //assert + //No exception thrown _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } [Test] public async Task StartIQNoConnectionTest() { - // Act & Assert + + //act await _client.StartIQAsync(); + //assert + //No exception thrown _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.Never); _tcpMock.VerifyGet(tcp => tcp.Connected, Times.AtLeastOnce); } @@ -80,29 +88,30 @@ public async Task StartIQNoConnectionTest() [Test] public async Task StartIQTest() { - // Arrange - await _client.ConnectAsync(); + //Arrange + await ConnectAsyncTest(); - // Act + //act await _client.StartIQAsync(); - // Assert - _udpMock.Verify(udp => udp.StartListeningAsync(), Times.Once); + //assert + //No exception thrown + _updMock.Verify(udp => udp.StartListeningAsync(), Times.Once); Assert.That(_client.IQStarted, Is.True); } [Test] public async Task StopIQTest() { - // Arrange - await _client.ConnectAsync(); - await _client.StartIQAsync(); + //Arrange + await ConnectAsyncTest(); - // Act + //act await _client.StopIQAsync(); - // Assert - _udpMock.Verify(udp => udp.StopListening(), Times.Once); + //assert + //No exception thrown + _updMock.Verify(tcp => tcp.StopListening(), Times.Once); Assert.That(_client.IQStarted, Is.False); } @@ -113,9 +122,32 @@ public async Task ChangeFrequencyAsync_SendsMessage() await _client.ConnectAsync(); // Act - await _client.ChangeFrequencyAsync(144000000, 1); + await _client.ChangeFrequencyAsync(144000000, 1); // 144 MHz, channel 1 // Assert _tcpMock.Verify(tcp => tcp.SendMessageAsync(It.IsAny()), Times.AtLeast(4)); } + + [Test] + public void TcpClient_MessageReceived_SetsResponseTask() + { + // Arrange + var bytes = new byte[] { 0x01, 0x02, 0x03 }; + var tcpClientField = typeof(NetSdrClient) + .GetField("responseTaskSource", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + var tcs = new TaskCompletionSource(); + tcpClientField.SetValue(_client, tcs); + + // Act + var method = typeof(NetSdrClient) + .GetMethod("_tcpClient_MessageReceived", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + method.Invoke(_client, new object?[] { null, bytes }); + + // Assert + Assert.IsTrue(tcs.Task.IsCompleted); + Assert.That(tcs.Task.Result, Is.EqualTo(bytes)); + } + //TODO: cover the rest of the NetSdrClient code here } \ No newline at end of file diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 2440537..58a1559 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -63,41 +63,6 @@ public void GetDataItemMessageTest() Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); } - [Test] - public void GetControlItemMessage_WithNullParameters_ThrowsException() - { - // Arrange - var type = NetSdrMessageHelper.MsgTypes.Ack; - var code = NetSdrMessageHelper.ControlItemCodes.ReceiverState; - - // Act & Assert - Assert.Throws(() => - NetSdrMessageHelper.GetControlItemMessage(type, code, null!)); - } - - [Test] - public void GetDataItemMessage_WithNullParameters_ThrowsException() - { - // Arrange - var type = NetSdrMessageHelper.MsgTypes.DataItem2; - // Act & Assert - Assert.Throws(() => - NetSdrMessageHelper.GetDataItemMessage(type, null!)); - } - - [Test] - public void GetControlItemMessage_WithEmptyParameters() - { - // Arrange - var type = NetSdrMessageHelper.MsgTypes.Ack; - var code = NetSdrMessageHelper.ControlItemCodes.ReceiverState; - - // Act - byte[] msg = NetSdrMessageHelper.GetControlItemMessage(type, code, Array.Empty()); - - // Assert - Assert.That(msg.Length, Is.EqualTo(4)); // Only header + code - } } } \ No newline at end of file From 2dd5ecac9560626e8ccc29f9fb5d29d71f3a9bca Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 23 Oct 2025 18:01:29 +0300 Subject: [PATCH 07/28] . --- NetSdrClientAppTests/NetSdrMessageHelperTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index 58a1559..b40fff7 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -64,5 +64,6 @@ public void GetDataItemMessageTest() Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); } + //TODO: add more NetSdrMessageHelper tests } } \ No newline at end of file From 96d21d6aabf39d41d4f6cd12fb88b595d2d3130c Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 15:03:56 +0200 Subject: [PATCH 08/28] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D1=96=D0=BD=D0=B3=20=D0=BA=D0=BE=D0=B4=D1=83=20Ech?= =?UTF-8?q?oServer=20=D1=82=D0=B0=20=D0=B4=D0=BE=D0=B4=D0=B0=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8F=20EchoServerTests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EchoServerTests/EchoServerTests.cs | 90 ++++++++ EchoServerTests/EchoServerTests.csproj | 29 +++ EchoServerTests/UdpTimedSenderTests.cs | 203 ++++++++++++++++++ EchoTspServer/EchoServer.cs | 82 +++++++ EchoTspServer/Interfaces/ILogger.cs | 7 + EchoTspServer/Interfaces/INetworkStream.cs | 12 ++ EchoTspServer/Interfaces/ITcpClient.cs | 10 + EchoTspServer/Interfaces/ITcpListener.cs | 11 + EchoTspServer/Program.cs | 181 +++------------- EchoTspServer/UdpTimedSender.cs | 68 ++++++ EchoTspServer/Wrappers/ConsoleLogger.cs | 13 ++ .../Wrappers/NetworkStreamWrapper.cs | 32 +++ EchoTspServer/Wrappers/TcpClientWrapper.cs | 30 +++ EchoTspServer/Wrappers/TcpListenerWrapper.cs | 33 +++ InfrastructureTests/ArchitectureTests.cs | 53 +++++ .../InfrastructureTests.csproj | 31 +++ NetSdrClient.sln | 12 ++ NetSdrClientApp/NetSdrClient.cs | 17 +- NetSdrClientApp/NetSdrClientApp.csproj | 3 + 19 files changed, 758 insertions(+), 159 deletions(-) create mode 100644 EchoServerTests/EchoServerTests.cs create mode 100644 EchoServerTests/EchoServerTests.csproj create mode 100644 EchoServerTests/UdpTimedSenderTests.cs create mode 100644 EchoTspServer/EchoServer.cs create mode 100644 EchoTspServer/Interfaces/ILogger.cs create mode 100644 EchoTspServer/Interfaces/INetworkStream.cs create mode 100644 EchoTspServer/Interfaces/ITcpClient.cs create mode 100644 EchoTspServer/Interfaces/ITcpListener.cs create mode 100644 EchoTspServer/UdpTimedSender.cs create mode 100644 EchoTspServer/Wrappers/ConsoleLogger.cs create mode 100644 EchoTspServer/Wrappers/NetworkStreamWrapper.cs create mode 100644 EchoTspServer/Wrappers/TcpClientWrapper.cs create mode 100644 EchoTspServer/Wrappers/TcpListenerWrapper.cs create mode 100644 InfrastructureTests/ArchitectureTests.cs create mode 100644 InfrastructureTests/InfrastructureTests.csproj diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs new file mode 100644 index 0000000..1d1aabe --- /dev/null +++ b/EchoServerTests/EchoServerTests.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; +using EchoServer.Abstractions; +using Moq; +using NUnit.Framework; + + +namespace EchoServerTests +{ + [TestFixture] + public class EchoServerTests + { + private Mock _mockListener; + private Mock _mockLogger; + private EchoServer.EchoServer _server; + + [SetUp] + public void Setup() + { + _mockListener = new Mock(); + _mockLogger = new Mock(); + _server = new EchoServer.EchoServer(_mockListener.Object, _mockLogger.Object); + } + + [Test] + public async Task StartAsync_ShouldStartListenerAndAcceptClients() + { + var mockClient = new Mock(); + var mockStream = new Mock(); + mockClient.Setup(c => c.GetStream()).Returns(mockStream.Object); + + _mockListener.SetupSequence(l => l.AcceptTcpClientAsync()) + .ReturnsAsync(mockClient.Object) + .ThrowsAsync(new OperationCanceledException()); + + await _server.StartAsync(); + + _mockListener.Verify(l => l.Start(), Times.Once); + _mockListener.Verify(l => l.AcceptTcpClientAsync(), Times.Exactly(2)); + _mockLogger.Verify(log => log.Log("Server started."), Times.Once); + _mockLogger.Verify(log => log.Log("Server shutdown."), Times.Once); + } + + [Test] + public void Stop_ShouldStopListenerAndCancelToken() + { + _server.Stop(); + + _mockListener.Verify(l => l.Stop(), Times.Once); + _mockLogger.Verify(log => log.Log("Server stopped."), Times.Once); + } + + [Test] + public async Task HandleClientAsync_ShouldLogErrorAndCloseClient_WhenStreamThrowsException() + { + var mockClient = new Mock(); + var mockStream = new Mock(); + var exceptionMessage = "Connection was forcibly closed."; + + mockClient.Setup(c => c.GetStream()).Returns(mockStream.Object); + + mockStream.Setup(s => s.ReadAsync(It.IsAny(), 0, It.IsAny(), It.IsAny())) + .ThrowsAsync(new IOException(exceptionMessage)); + + await _server.HandleClientAsync(mockClient.Object, CancellationToken.None); + + mockStream.Verify(s => s.WriteAsync(It.IsAny(), 0, It.IsAny(), It.IsAny()), Times.Never); + _mockLogger.Verify(log => log.Log($"Error: {exceptionMessage}"), Times.Once); + mockClient.Verify(c => c.Close(), Times.Once); + _mockLogger.Verify(log => log.Log("Client disconnected."), Times.Once); + } + + [Test] + public async Task StartAsync_ShouldStopGracefully_WhenListenerThrowsObjectDisposedException() + { + _mockListener.Setup(l => l.AcceptTcpClientAsync()).ThrowsAsync(new ObjectDisposedException("TcpListener")); + + await _server.StartAsync(); + + _mockListener.Verify(l => l.Start(), Times.Once); + _mockLogger.Verify(log => log.Log("Server started."), Times.Once); + _mockLogger.Verify(log => log.Log("Server shutdown."), Times.Once); + } + } +} \ No newline at end of file diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj new file mode 100644 index 0000000..f15c0de --- /dev/null +++ b/EchoServerTests/EchoServerTests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/EchoServerTests/UdpTimedSenderTests.cs b/EchoServerTests/UdpTimedSenderTests.cs new file mode 100644 index 0000000..cf018ac --- /dev/null +++ b/EchoServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,203 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using EchoServer; +using NUnit.Framework; + +namespace EchoServerTests +{ + [TestFixture] + public class UdpTimedSenderTests + { + private const int ReceiveTimeoutMs = 2000; + private UdpClient? _listener; + private int _port; + private UdpTimedSender? _sender; + + [SetUp] + public void SetUp() + { + _listener = new UdpClient(new IPEndPoint(IPAddress.Loopback, 0)); // ephemeral port + _port = ((IPEndPoint)_listener.Client.LocalEndPoint!).Port; + } + + [TearDown] + public void TearDown() + { + try + { + _sender?.StopSending(); + _sender?.Dispose(); + } + catch + { + // ignore cleanup exceptions + } + + try + { + _listener?.Close(); + _listener?.Dispose(); + } + catch + { + // ignore + } + } + + private static async Task ReceiveWithTimeoutAsync(UdpClient listener, int timeoutMs) + { + var receiveTask = listener.ReceiveAsync(); + var delayTask = Task.Delay(timeoutMs); + var completed = await Task.WhenAny(receiveTask, delayTask).ConfigureAwait(false); + if (completed == receiveTask) + { + return receiveTask.Result; + } + return null; + } + + [Test] + public async Task StartSending_SendsUdpMessage_WithExpectedFormat() + { + // Arrange + _sender = new UdpTimedSender("127.0.0.1", _port); + + try + { + // Act + _sender.StartSending(50); // small interval + + var received = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); + Assert.IsNotNull(received, "No UDP message received within timeout."); + + // Assert on message format + var data = received!.Value.Buffer; + // expected minimum size: 2(header) + 2(seq) + payload (1024) + Assert.GreaterOrEqual(data.Length, 2 + 2 + 1, "Received data too short."); + + Assert.AreEqual(0x04, data[0], "First header byte mismatch."); + Assert.AreEqual(0x84, data[1], "Second header byte mismatch."); + + ushort seq = BitConverter.ToUInt16(data, 2); + // In implementation i is incremented before sending; first send -> seq == 1 + Assert.AreEqual((ushort)1, seq, "Sequence number of first message should be 1."); + + // Total length should be 2 + 2 + 1024 = 1028 bytes + Assert.GreaterOrEqual(data.Length, 1028, "Expected message length at least 1028 bytes."); + } + finally + { + _sender?.StopSending(); + _sender?.Dispose(); + _sender = null; + } + } + + [Test] + public void StartSending_Throws_WhenAlreadyRunning() + { + // Arrange + _sender = new UdpTimedSender("127.0.0.1", _port); + + try + { + // Act + _sender.StartSending(100); + + // Assert second start throws InvalidOperationException + var ex = Assert.Throws(() => _sender!.StartSending(100)); + Assert.IsTrue(ex!.Message.IndexOf("already running", StringComparison.OrdinalIgnoreCase) >= 0); + } + finally + { + _sender?.StopSending(); + _sender?.Dispose(); + _sender = null; + } + } + + [Test] + public async Task StopSending_StopsFurtherMessages() + { + // Arrange + _sender = new UdpTimedSender("127.0.0.1", _port); + + try + { + _sender.StartSending(50); + + // receive at least one message + var first = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); + Assert.IsNotNull(first, "Expected to receive at least one message after start."); + + // Now stop the sender + _sender.StopSending(); + + // Try to receive another message within a short time - should time out (no more sends) + var second = await ReceiveWithTimeoutAsync(_listener!, 500); + Assert.IsNull(second, "No further messages expected after StopSending."); + } + finally + { + _sender?.StopSending(); + _sender?.Dispose(); + _sender = null; + } + } + + [Test] + public async Task Dispose_StopsAndDisposesResources_NoExceptions() + { + // Arrange + _sender = new UdpTimedSender("127.0.0.1", _port); + + // Act + _sender.StartSending(50); + + // give it a little time to send something + var received = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); + Assert.IsNotNull(received, "Expected message before dispose."); + + // Dispose should stop sending and not throw + Assert.DoesNotThrow(() => _sender!.Dispose()); + + // After dispose there should be no more messages; try receive short timeout + var afterDispose = await ReceiveWithTimeoutAsync(_listener!, 300); + Assert.IsNull(afterDispose, "No messages expected after Dispose."); + _sender = null; // already disposed + } + + [Test] + public async Task Messages_Sequence_IncrementsAcrossSends() + { + // Arrange + _sender = new UdpTimedSender("127.0.0.1", _port); + + try + { + _sender.StartSending(50); + + // Receive first two messages + var first = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); + Assert.IsNotNull(first, "First message not received."); + var firstSeq = BitConverter.ToUInt16(first!.Value.Buffer, 2); + + var second = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); + Assert.IsNotNull(second, "Second message not received."); + var secondSeq = BitConverter.ToUInt16(second!.Value.Buffer, 2); + + Assert.AreEqual((ushort)(firstSeq + 1), secondSeq, "Sequence should increment by 1."); + } + finally + { + _sender?.StopSending(); + _sender?.Dispose(); + _sender = null; + } + } + } +} \ No newline at end of file diff --git a/EchoTspServer/EchoServer.cs b/EchoTspServer/EchoServer.cs new file mode 100644 index 0000000..89b8e66 --- /dev/null +++ b/EchoTspServer/EchoServer.cs @@ -0,0 +1,82 @@ +using EchoServer.Abstractions; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoServer; + +public class EchoServer +{ + private readonly ITcpListener _listener; + private readonly ILogger _logger; + private readonly CancellationTokenSource _cancellationTokenSource; + + public EchoServer(ITcpListener listener, ILogger logger) + { + _listener = listener; + _logger = logger; + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async Task StartAsync() + { + _listener.Start(); + _logger.Log("Server started."); + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + ITcpClient client = await _listener.AcceptTcpClientAsync(); + _logger.Log("Client connected."); + + _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); + } + catch (Exception ex) when (ex is ObjectDisposedException || ex is OperationCanceledException) + { + // Listener has been closed or operation was cancelled + break; + } + } + _logger.Log("Server shutdown."); + } + + public async Task HandleClientAsync(ITcpClient client, CancellationToken token) + { + using (client) + using (INetworkStream 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); + _logger.Log($"Echoed {bytesRead} bytes to the client."); + } + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + _logger.Log($"Error: {ex.Message}"); + } + finally + { + client.Close(); + _logger.Log("Client disconnected."); + } + } + } + + public void Stop() + { + if (!_cancellationTokenSource.IsCancellationRequested) + { + _cancellationTokenSource.Cancel(); + } + _listener.Stop(); + _cancellationTokenSource.Dispose(); + _logger.Log("Server stopped."); + } +} \ No newline at end of file diff --git a/EchoTspServer/Interfaces/ILogger.cs b/EchoTspServer/Interfaces/ILogger.cs new file mode 100644 index 0000000..1446331 --- /dev/null +++ b/EchoTspServer/Interfaces/ILogger.cs @@ -0,0 +1,7 @@ +namespace EchoServer.Abstractions +{ + public interface ILogger + { + void Log(string message); + } +} \ No newline at end of file diff --git a/EchoTspServer/Interfaces/INetworkStream.cs b/EchoTspServer/Interfaces/INetworkStream.cs new file mode 100644 index 0000000..5d3a79c --- /dev/null +++ b/EchoTspServer/Interfaces/INetworkStream.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoServer.Abstractions +{ + public interface INetworkStream : IDisposable + { + Task ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken); + Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/EchoTspServer/Interfaces/ITcpClient.cs b/EchoTspServer/Interfaces/ITcpClient.cs new file mode 100644 index 0000000..4094e88 --- /dev/null +++ b/EchoTspServer/Interfaces/ITcpClient.cs @@ -0,0 +1,10 @@ +using System; + +namespace EchoServer.Abstractions +{ + public interface ITcpClient : IDisposable + { + INetworkStream GetStream(); + void Close(); + } +} \ No newline at end of file diff --git a/EchoTspServer/Interfaces/ITcpListener.cs b/EchoTspServer/Interfaces/ITcpListener.cs new file mode 100644 index 0000000..f3d59be --- /dev/null +++ b/EchoTspServer/Interfaces/ITcpListener.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace EchoServer.Abstractions +{ + public interface ITcpListener + { + void Start(); + Task AcceptTcpClientAsync(); + void Stop(); + } +} \ No newline at end of file diff --git a/EchoTspServer/Program.cs b/EchoTspServer/Program.cs index 4efafc0..0471fa7 100644 --- a/EchoTspServer/Program.cs +++ b/EchoTspServer/Program.cs @@ -1,174 +1,43 @@ -using System; +using EchoServer.Wrappers; using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; +using System; using System.Threading.Tasks; +using EchoServer; - -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer +namespace EchoServer { - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; - - - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task StartAsync() + public static class Program { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) + public static Task Main(string[] args) { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); + int serverPort = 5000; + var logger = new ConsoleLogger(); + var listener = new TcpListenerWrapper(IPAddress.Any, serverPort); - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } + var server = new EchoServer(listener, logger); - Console.WriteLine("Server shutdown."); - } + _ = Task.Run(() => server.StartAsync()); - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try + string host = "127.0.0.1"; + int udpPort = 60000; + int intervalMilliseconds = 5000; + + using (var sender = new UdpTimedSender(host, udpPort)) { - byte[] buffer = new byte[8192]; - int bytesRead; + logger.Log("Press 'q' to stop the server and sender..."); + sender.StartSending(intervalMilliseconds); - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + 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."); + // Just wait until 'q' is pressed } - } - 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(); + logger.Log("Sender and server stopped."); } - sender.StopSending(); - server.Stop(); - Console.WriteLine("Sender stopped."); + return Task.CompletedTask; } } -} - - -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/EchoTspServer/UdpTimedSender.cs b/EchoTspServer/UdpTimedSender.cs new file mode 100644 index 0000000..e35419a --- /dev/null +++ b/EchoTspServer/UdpTimedSender.cs @@ -0,0 +1,68 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoServer; + +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 + byte[] samples = new byte[1024]; + RandomNumberGenerator.Fill(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/EchoTspServer/Wrappers/ConsoleLogger.cs b/EchoTspServer/Wrappers/ConsoleLogger.cs new file mode 100644 index 0000000..dce6d42 --- /dev/null +++ b/EchoTspServer/Wrappers/ConsoleLogger.cs @@ -0,0 +1,13 @@ +using EchoServer.Abstractions; +using System; + +namespace EchoServer.Wrappers +{ + public class ConsoleLogger : ILogger + { + public void Log(string message) + { + Console.WriteLine(message); + } + } +} \ No newline at end of file diff --git a/EchoTspServer/Wrappers/NetworkStreamWrapper.cs b/EchoTspServer/Wrappers/NetworkStreamWrapper.cs new file mode 100644 index 0000000..3010f73 --- /dev/null +++ b/EchoTspServer/Wrappers/NetworkStreamWrapper.cs @@ -0,0 +1,32 @@ +using EchoServer.Abstractions; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoServer.Wrappers +{ + public class NetworkStreamWrapper : INetworkStream + { + private readonly NetworkStream _stream; + + public NetworkStreamWrapper(NetworkStream stream) + { + _stream = stream; + } + + public Task ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + return _stream.ReadAsync(buffer, offset, size, cancellationToken); + } + + public Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + return _stream.WriteAsync(buffer, offset, size, cancellationToken); + } + + public void Dispose() + { + _stream.Dispose(); + } + } +} \ No newline at end of file diff --git a/EchoTspServer/Wrappers/TcpClientWrapper.cs b/EchoTspServer/Wrappers/TcpClientWrapper.cs new file mode 100644 index 0000000..e7b3aaf --- /dev/null +++ b/EchoTspServer/Wrappers/TcpClientWrapper.cs @@ -0,0 +1,30 @@ +using EchoServer.Abstractions; +using System.Net.Sockets; + +namespace EchoServer.Wrappers +{ + public class TcpClientWrapper : ITcpClient + { + private readonly TcpClient _client; + + public TcpClientWrapper(TcpClient client) + { + _client = client; + } + + public INetworkStream GetStream() + { + return new NetworkStreamWrapper(_client.GetStream()); + } + + public void Close() + { + _client.Close(); + } + + public void Dispose() + { + _client.Dispose(); + } + } +} \ No newline at end of file diff --git a/EchoTspServer/Wrappers/TcpListenerWrapper.cs b/EchoTspServer/Wrappers/TcpListenerWrapper.cs new file mode 100644 index 0000000..8ec85d5 --- /dev/null +++ b/EchoTspServer/Wrappers/TcpListenerWrapper.cs @@ -0,0 +1,33 @@ +using EchoServer.Abstractions; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace EchoServer.Wrappers +{ + public class TcpListenerWrapper : ITcpListener + { + private readonly TcpListener _listener; + + public TcpListenerWrapper(IPAddress address, int port) + { + _listener = new TcpListener(address, port); + } + + public async Task AcceptTcpClientAsync() + { + var tcpClient = await _listener.AcceptTcpClientAsync(); + return new TcpClientWrapper(tcpClient); + } + + public void Start() + { + _listener.Start(); + } + + public void Stop() + { + _listener.Stop(); + } + } +} \ No newline at end of file diff --git a/InfrastructureTests/ArchitectureTests.cs b/InfrastructureTests/ArchitectureTests.cs new file mode 100644 index 0000000..ba01071 --- /dev/null +++ b/InfrastructureTests/ArchitectureTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using NetArchTest.Rules; + +namespace InfrastructureTests +{ + [TestFixture] + public class ArchitectureTests + { + private readonly Assembly _uiAssembly; + private readonly Assembly _infrastructureAssembly; + + public ArchitectureTests() + { + _uiAssembly = Assembly.Load("NetSdrClientApp"); + _infrastructureAssembly = Assembly.Load("EchoServer"); + } + + [Test] + public void UI_Should_Not_Depend_On_Infrastructure() + { + var result = Types + .InAssembly(_uiAssembly) + .ShouldNot() + .HaveDependencyOn(_infrastructureAssembly.GetName().Name) + .GetResult(); + + var failing = result.FailingTypeNames ?? new string[0]; + + Assert.That(result.IsSuccessful, + $"UI шар не повинен залежати від Infrastructure. Порушення: {(failing.Count == 0 ? "немає" : string.Join(", ", failing))}"); + } + + [Test] + public void Infrastructure_Should_Not_Depend_On_UI() + { + var result = Types + .InAssembly(_infrastructureAssembly) + .ShouldNot() + .HaveDependencyOn(_uiAssembly.GetName().Name) + .GetResult(); + + var failing = result.FailingTypeNames ?? new string[0]; + + Assert.That(result.IsSuccessful, + $"Infrastructure не повинен залежати від UI. Порушення: {(failing.Count == 0 ? "немає" : string.Join(", ", failing))}"); + } + } +} diff --git a/InfrastructureTests/InfrastructureTests.csproj b/InfrastructureTests/InfrastructureTests.csproj new file mode 100644 index 0000000..5c51e1c --- /dev/null +++ b/InfrastructureTests/InfrastructureTests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/NetSdrClient.sln b/NetSdrClient.sln index d8ca20f..ff3a383 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "Net EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTspServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfrastructureTests", "InfrastructureTests\InfrastructureTests.csproj", "{9C23A201-5494-4F05-B3B6-D93796D0A36A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServerTests", "EchoServerTests\EchoServerTests.csproj", "{7730FCEA-FB36-4FAC-B882-F143561EE160}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,14 @@ 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 + {9C23A201-5494-4F05-B3B6-D93796D0A36A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C23A201-5494-4F05-B3B6-D93796D0A36A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C23A201-5494-4F05-B3B6-D93796D0A36A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C23A201-5494-4F05-B3B6-D93796D0A36A}.Release|Any CPU.Build.0 = Release|Any CPU + {7730FCEA-FB36-4FAC-B882-F143561EE160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7730FCEA-FB36-4FAC-B882-F143561EE160}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7730FCEA-FB36-4FAC-B882-F143561EE160}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7730FCEA-FB36-4FAC-B882-F143561EE160}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index 496a612..da0378e 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,5 +1,3 @@ -using NetSdrClientApp.Messages; -using NetSdrClientApp.Networking; using System; using System.Collections.Generic; using System.Linq; @@ -7,11 +5,22 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -using static NetSdrClientApp.Messages.NetSdrMessageHelper; +using NetSdrClientApp.Messages; +using NetSdrClientApp.Networking; using static System.Runtime.InteropServices.JavaScript.JSType; +using EchoServer; +using static NetSdrClientApp.Messages.NetSdrMessageHelper; namespace NetSdrClientApp { + //public class WrongDependency + //{ + // public string UseInfrastructure() + // { + // var m = new InfrastructureMarker(); + // return m.Name; + // } + //} public class NetSdrClient { private ITcpClient _tcpClient; @@ -19,6 +28,7 @@ public class NetSdrClient public bool IQStarted { get; set; } + public NetSdrClient(ITcpClient tcpClient, IUdpClient udpClient) { _tcpClient = tcpClient; @@ -28,6 +38,7 @@ public NetSdrClient(ITcpClient tcpClient, IUdpClient udpClient) _udpClient.MessageReceived += _udpClient_MessageReceived; } + public async Task ConnectAsync() { if (!_tcpClient.Connected) diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj index 2ac9100..7628deb 100644 --- a/NetSdrClientApp/NetSdrClientApp.csproj +++ b/NetSdrClientApp/NetSdrClientApp.csproj @@ -10,5 +10,8 @@ + + + From c204f007f010d72ba3d00dc1e6c401a942b71b2b Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 15:10:30 +0200 Subject: [PATCH 09/28] Update coverage report paths in SonarCloud workflow --- .github/workflows/sonarcloud.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 02ba1c6..93f66f7 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -60,7 +60,7 @@ jobs: /k:"Missile2006_NetSdrClient" ` /o:"missile2006" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` - /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` + /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` /d:sonar.cpd.cs.minimumLines=5 ` /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` @@ -78,11 +78,18 @@ jobs: - name: Tests with coverage (OpenCover) run: | - dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` + dotnet test NetSdrClient.sln ` + --configuration Release ` + --no-build ` /p:CollectCoverage=true ` - /p:CoverletOutput=TestResults/coverage.xml ` - /p:CoverletOutputFormat=opencover + /p:CoverletOutputFormat=opencover ` + /p:CoverletOutput=TestResults/coverage.opencover.xml shell: pwsh + - name: Show coverage files + run: Get-ChildItem -Recurse TestResults + shell: pwsh + - name: Show all coverage.opencover.xml + run: Get-ChildItem -Recurse -Filter coverage.opencover.xml # 3) END: SonarScanner - name: SonarScanner End From 726f4fbff8a68d1080e107f876540981ac673333 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 15:44:36 +0200 Subject: [PATCH 10/28] . --- NetSdrClientApp/Networking/UdpClientWrapper.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 91bb71e..7e6ea41 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -57,12 +57,7 @@ public void Exit() 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); + return HashCode.Combine(_localEndPoint.Address?.ToString(), _localEndPoint.Port); } public override bool Equals(object? obj) From 80067c3460eaad197fe49aaee3019958e93b3b6f Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 15:50:26 +0200 Subject: [PATCH 11/28] Fix indentation and shell declaration in sonarcloud.yml --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 93f66f7..ce4ca9a 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -90,7 +90,7 @@ jobs: shell: pwsh - name: Show all coverage.opencover.xml run: Get-ChildItem -Recurse -Filter coverage.opencover.xml - + shell: pwsh # 3) END: SonarScanner - name: SonarScanner End run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" From 57e8f3947ed7cf5fac5c9e057dda0384343971c8 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 17:08:19 +0200 Subject: [PATCH 12/28] Add Dependabot configuration for NuGet updates --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml 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" From ae6223bae942e3beaadac4fc8cee95f1a549dceb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:11:04 +0000 Subject: [PATCH 13/28] Bump coverlet.collector from 6.0.0 to 6.0.4 --- updated-dependencies: - dependency-name: coverlet.collector dependency-version: 6.0.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: coverlet.collector dependency-version: 6.0.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: coverlet.collector dependency-version: 6.0.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- EchoServerTests/EchoServerTests.csproj | 2 +- InfrastructureTests/InfrastructureTests.csproj | 2 +- NetSdrClientAppTests/NetSdrClientAppTests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj index f15c0de..3a9d059 100644 --- a/EchoServerTests/EchoServerTests.csproj +++ b/EchoServerTests/EchoServerTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/InfrastructureTests/InfrastructureTests.csproj b/InfrastructureTests/InfrastructureTests.csproj index 5c51e1c..8997a17 100644 --- a/InfrastructureTests/InfrastructureTests.csproj +++ b/InfrastructureTests/InfrastructureTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 38f437e..1fa5868 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 7e53ecafde39688856098a8c84f93716dd7c1980 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:12:20 +0000 Subject: [PATCH 14/28] Bump Microsoft.NET.Test.Sdk from 18.0.0 to 18.0.1 --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.1 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- InfrastructureTests/InfrastructureTests.csproj | 2 +- NetSdrClientAppTests/NetSdrClientAppTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InfrastructureTests/InfrastructureTests.csproj b/InfrastructureTests/InfrastructureTests.csproj index 5c51e1c..8c033d7 100644 --- a/InfrastructureTests/InfrastructureTests.csproj +++ b/InfrastructureTests/InfrastructureTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 38f437e..70da4a8 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -15,7 +15,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 92922d5c688c564942551e81ba760399ec1ac810 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:14:37 +0000 Subject: [PATCH 15/28] Bump NUnit.Analyzers from 3.9.0 to 4.11.2 --- updated-dependencies: - dependency-name: NUnit.Analyzers dependency-version: 4.11.2 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: NUnit.Analyzers dependency-version: 4.11.2 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: NUnit.Analyzers dependency-version: 4.11.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- EchoServerTests/EchoServerTests.csproj | 2 +- InfrastructureTests/InfrastructureTests.csproj | 2 +- NetSdrClientAppTests/NetSdrClientAppTests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj index f15c0de..2435357 100644 --- a/EchoServerTests/EchoServerTests.csproj +++ b/EchoServerTests/EchoServerTests.csproj @@ -14,7 +14,7 @@ - + diff --git a/InfrastructureTests/InfrastructureTests.csproj b/InfrastructureTests/InfrastructureTests.csproj index 5c51e1c..cde7084 100644 --- a/InfrastructureTests/InfrastructureTests.csproj +++ b/InfrastructureTests/InfrastructureTests.csproj @@ -15,7 +15,7 @@ - + diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 38f437e..a3bf248 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -18,7 +18,7 @@ - + From 19adf2a25c10bc39a4cb9c8ab4a4f083a122585e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:15:25 +0000 Subject: [PATCH 16/28] Bump NUnit3TestAdapter from 4.5.0 to 5.2.0 --- updated-dependencies: - dependency-name: NUnit3TestAdapter dependency-version: 5.2.0 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: NUnit3TestAdapter dependency-version: 5.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- EchoServerTests/EchoServerTests.csproj | 2 +- NetSdrClientAppTests/NetSdrClientAppTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj index f15c0de..c016fd9 100644 --- a/EchoServerTests/EchoServerTests.csproj +++ b/EchoServerTests/EchoServerTests.csproj @@ -15,7 +15,7 @@ - + diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 38f437e..d668551 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -19,7 +19,7 @@ - + From b7b67ad085941756043413fde8cf8953dac61de6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:15:44 +0000 Subject: [PATCH 17/28] Bump SharpZipLib from 1.3.2 to 1.4.2 --- updated-dependencies: - dependency-name: SharpZipLib dependency-version: 1.4.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- NetSdrClientApp/NetSdrClientApp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj index 7628deb..fd8618e 100644 --- a/NetSdrClientApp/NetSdrClientApp.csproj +++ b/NetSdrClientApp/NetSdrClientApp.csproj @@ -8,7 +8,7 @@ - + From 5312c4a1d724b97a56cf604a2899bc0eb982cb0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:16:30 +0000 Subject: [PATCH 18/28] Bump NUnit from 3.14.0 to 4.4.0 --- updated-dependencies: - dependency-name: NUnit dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: NUnit dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- EchoServerTests/EchoServerTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj index 3a9d059..02c11e3 100644 --- a/EchoServerTests/EchoServerTests.csproj +++ b/EchoServerTests/EchoServerTests.csproj @@ -13,7 +13,7 @@ - + From e2de9a240e73ad08324b68d5303848ec5e3b3b0a Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 17:33:04 +0200 Subject: [PATCH 19/28] Update coverage report paths in SonarCloud workflow --- .github/workflows/sonarcloud.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index ce4ca9a..5a6188c 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -60,7 +60,7 @@ jobs: /k:"Missile2006_NetSdrClient" ` /o:"missile2006" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` - /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" ` + /d:sonar.cs.opencover.reportsPaths="**/TestResults/coverage.xml" ` /d:sonar.cpd.cs.minimumTokens=40 ` /d:sonar.cpd.cs.minimumLines=5 ` /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` @@ -78,12 +78,15 @@ jobs: - name: Tests with coverage (OpenCover) run: | - dotnet test NetSdrClient.sln ` - --configuration Release ` - --no-build ` + dotnet test EchoServerTests/EchoServerTests.csproj -c Release --no-build ` /p:CollectCoverage=true ` - /p:CoverletOutputFormat=opencover ` - /p:CoverletOutput=TestResults/coverage.opencover.xml + /p:CoverletOutput=EchoServerTests/TestResults/coverage.xml ` + /p:CoverletOutputFormat=opencover + + dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` + /p:CollectCoverage=true ` + /p:CoverletOutput=NetSdrClientAppTests/TestResults/coverage.xml ` + /p:CoverletOutputFormat=opencover shell: pwsh - name: Show coverage files run: Get-ChildItem -Recurse TestResults From 0ecd92ab124800fb17eaa0cf6125b39fd10ba358 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 17:40:47 +0200 Subject: [PATCH 20/28] Fix coverage collection in sonarcloud.yml Updated test commands to collect coverage for EchoServerTests and EchoTspServerTests. --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 5a6188c..2ab8128 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -78,9 +78,9 @@ jobs: - name: Tests with coverage (OpenCover) run: | - dotnet test EchoServerTests/EchoServerTests.csproj -c Release --no-build ` + dotnet test EchoTspServerTests/EchoServerTests.csproj -c Release --no-build ` /p:CollectCoverage=true ` - /p:CoverletOutput=EchoServerTests/TestResults/coverage.xml ` + /p:CoverletOutput=EchoTspServerTests/TestResults/coverage.xml ` /p:CoverletOutputFormat=opencover dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` From 6e31f0929776d0783b7281299485a06177e56113 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 17:48:18 +0200 Subject: [PATCH 21/28] Add coverlet.msbuild package reference --- EchoServerTests/EchoServerTests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj index a5cd752..1d898da 100644 --- a/EchoServerTests/EchoServerTests.csproj +++ b/EchoServerTests/EchoServerTests.csproj @@ -11,6 +11,7 @@ + From 1998b72eb7f3a3e19d353e58d5eabe9399b567d1 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 17:53:02 +0200 Subject: [PATCH 22/28] Fix coverage output paths in sonarcloud.yml Updated test commands to correctly output coverage files for both EchoTspServerTests and EchoServerTests. --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 2ab8128..5a6188c 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -78,9 +78,9 @@ jobs: - name: Tests with coverage (OpenCover) run: | - dotnet test EchoTspServerTests/EchoServerTests.csproj -c Release --no-build ` + dotnet test EchoServerTests/EchoServerTests.csproj -c Release --no-build ` /p:CollectCoverage=true ` - /p:CoverletOutput=EchoTspServerTests/TestResults/coverage.xml ` + /p:CoverletOutput=EchoServerTests/TestResults/coverage.xml ` /p:CoverletOutputFormat=opencover dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` From 6866e7296597568a8483751664b498bb24f01915 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 23:12:59 +0200 Subject: [PATCH 23/28] . --- EchoServerTests/UdpTimedSenderTests.cs | 44 +++-- EchoTspServer/Program.cs | 6 +- EchoTspServer/UdpTimedSender.cs | 121 +++++++----- EchoTspServer/Wrappers/ConsoleLogger.cs | 6 +- .../Wrappers/NetworkStreamWrapper.cs | 38 +++- EchoTspServer/Wrappers/TcpClientWrapper.cs | 36 +++- InfrastructureTests/ArchitectureTests.cs | 11 +- .../Messages/NetSdrMessageHelper.cs | 1 - NetSdrClientApp/NetSdrClient.cs | 51 ++--- NetSdrClientApp/Networking/IUdpClient.cs | 15 +- .../Networking/TcpClientWrapper.cs | 176 +++++++++++++----- .../Networking/UdpClientWrapper.cs | 81 +++++--- NetSdrClientAppTests/NetSdrClientTests.cs | 27 +-- .../NetSdrMessageHelperTests.cs | 57 +++--- 14 files changed, 437 insertions(+), 233 deletions(-) diff --git a/EchoServerTests/UdpTimedSenderTests.cs b/EchoServerTests/UdpTimedSenderTests.cs index cf018ac..c506802 100644 --- a/EchoServerTests/UdpTimedSenderTests.cs +++ b/EchoServerTests/UdpTimedSenderTests.cs @@ -70,24 +70,29 @@ public async Task StartSending_SendsUdpMessage_WithExpectedFormat() { // Act _sender.StartSending(50); // small interval - var received = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); - Assert.IsNotNull(received, "No UDP message received within timeout."); - // Assert on message format + // Assert using Constraint Model + Assert.That(received, Is.Not.Null, "No UDP message received within timeout."); + var data = received!.Value.Buffer; - // expected minimum size: 2(header) + 2(seq) + payload (1024) - Assert.GreaterOrEqual(data.Length, 2 + 2 + 1, "Received data too short."); - Assert.AreEqual(0x04, data[0], "First header byte mismatch."); - Assert.AreEqual(0x84, data[1], "Second header byte mismatch."); + // expected minimum size: 2(header) + 2(seq) + payload (>=1) + Assert.That(data.Length, Is.GreaterThanOrEqualTo(2 + 2 + 1), + "Received data too short."); + + Assert.That(data[0], Is.EqualTo(0x04), "First header byte mismatch."); + Assert.That(data[1], Is.EqualTo(0x84), "Second header byte mismatch."); ushort seq = BitConverter.ToUInt16(data, 2); - // In implementation i is incremented before sending; first send -> seq == 1 - Assert.AreEqual((ushort)1, seq, "Sequence number of first message should be 1."); - // Total length should be 2 + 2 + 1024 = 1028 bytes - Assert.GreaterOrEqual(data.Length, 1028, "Expected message length at least 1028 bytes."); + // first message should have seq == 1 + Assert.That(seq, Is.EqualTo((ushort)1), + "Sequence number of first message should be 1."); + + // Expected total >= 1028 bytes + Assert.That(data.Length, Is.GreaterThanOrEqualTo(1028), + "Expected message length at least 1028 bytes."); } finally { @@ -97,6 +102,7 @@ public async Task StartSending_SendsUdpMessage_WithExpectedFormat() } } + [Test] public void StartSending_Throws_WhenAlreadyRunning() { @@ -110,7 +116,7 @@ public void StartSending_Throws_WhenAlreadyRunning() // Assert second start throws InvalidOperationException var ex = Assert.Throws(() => _sender!.StartSending(100)); - Assert.IsTrue(ex!.Message.IndexOf("already running", StringComparison.OrdinalIgnoreCase) >= 0); + Assert.That(ex!.Message.IndexOf("already running", StringComparison.OrdinalIgnoreCase) >= 0, Is.True); } finally { @@ -132,14 +138,14 @@ public async Task StopSending_StopsFurtherMessages() // receive at least one message var first = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); - Assert.IsNotNull(first, "Expected to receive at least one message after start."); + Assert.That(first, Is.Not.Null, "Expected to receive at least one message after start."); // Now stop the sender _sender.StopSending(); // Try to receive another message within a short time - should time out (no more sends) var second = await ReceiveWithTimeoutAsync(_listener!, 500); - Assert.IsNull(second, "No further messages expected after StopSending."); + Assert.That(second, Is.Null, "No further messages expected after StopSending."); } finally { @@ -160,14 +166,14 @@ public async Task Dispose_StopsAndDisposesResources_NoExceptions() // give it a little time to send something var received = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); - Assert.IsNotNull(received, "Expected message before dispose."); + Assert.That(received, Is.Not.Null, "Expected message before dispose."); // Dispose should stop sending and not throw Assert.DoesNotThrow(() => _sender!.Dispose()); // After dispose there should be no more messages; try receive short timeout var afterDispose = await ReceiveWithTimeoutAsync(_listener!, 300); - Assert.IsNull(afterDispose, "No messages expected after Dispose."); + Assert.That(afterDispose, Is.Null, "No messages expected after Dispose."); _sender = null; // already disposed } @@ -183,14 +189,14 @@ public async Task Messages_Sequence_IncrementsAcrossSends() // Receive first two messages var first = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); - Assert.IsNotNull(first, "First message not received."); + Assert.That(first, Is.Not.Null, "First message not received."); var firstSeq = BitConverter.ToUInt16(first!.Value.Buffer, 2); var second = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); - Assert.IsNotNull(second, "Second message not received."); + Assert.That(second, Is.Not.Null, "Second message not received."); var secondSeq = BitConverter.ToUInt16(second!.Value.Buffer, 2); - Assert.AreEqual((ushort)(firstSeq + 1), secondSeq, "Sequence should increment by 1."); + Assert.That(secondSeq, Is.EqualTo((ushort)(firstSeq + 1)), "Sequence should increment by 1."); } finally { diff --git a/EchoTspServer/Program.cs b/EchoTspServer/Program.cs index 0471fa7..8f2f27a 100644 --- a/EchoTspServer/Program.cs +++ b/EchoTspServer/Program.cs @@ -1,11 +1,13 @@ -using EchoServer.Wrappers; +using System; +using System.Diagnostics.CodeAnalysis; using System.Net; -using System; using System.Threading.Tasks; using EchoServer; +using EchoServer.Wrappers; namespace EchoServer { + [ExcludeFromCodeCoverage] public static class Program { public static Task Main(string[] args) diff --git a/EchoTspServer/UdpTimedSender.cs b/EchoTspServer/UdpTimedSender.cs index e35419a..ff97ec4 100644 --- a/EchoTspServer/UdpTimedSender.cs +++ b/EchoTspServer/UdpTimedSender.cs @@ -1,68 +1,97 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; -using System.Text; using System.Threading; -using System.Threading.Tasks; -namespace EchoServer; - -public class UdpTimedSender : IDisposable +namespace EchoServer { - 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; // nullable + private ushort _sequence = 0; + private bool _disposed = false; // track disposal - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); + public UdpTimedSender(string host, int port) + { + _host = host ?? throw new ArgumentNullException(nameof(host)); + _port = port; + _udpClient = new UdpClient(); + } - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } + public void StartSending(int intervalMilliseconds) + { + if (_disposed) + throw new ObjectDisposedException(nameof(UdpTimedSender)); - ushort i = 0; + if (_timer != null) + throw new InvalidOperationException("Sender is already running."); - private void SendMessageCallback(object state) - { - try + _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); + } + + private void SendMessageCallback(object? state) { - //dummy data - byte[] samples = new byte[1024]; - RandomNumberGenerator.Fill(samples); + try + { + byte[] samples = new byte[1024]; + RandomNumberGenerator.Fill(samples); + _sequence++; - i++; + byte[] msg = (new byte[] { 0x04, 0x84 }) + .Concat(BitConverter.GetBytes(_sequence)) + .Concat(samples) + .ToArray(); - byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray(); - var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); + var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); + _udpClient.Send(msg, msg.Length, endpoint); - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port} "); + Console.WriteLine($"Message sent to {_host}:{_port}, seq={_sequence}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error sending message: {ex.Message}"); + } } - catch (Exception ex) + + public void StopSending() { - Console.WriteLine($"Error sending message: {ex.Message}"); + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } } - } - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } + #region IDisposable Support + [ExcludeFromCodeCoverage] + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Dispose managed resources + StopSending(); + _udpClient.Dispose(); + } - public void Dispose() - { - StopSending(); - _udpClient.Dispose(); + // No unmanaged resources to free here + + _disposed = true; + } + } + [ExcludeFromCodeCoverage] + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion } -} \ No newline at end of file +} diff --git a/EchoTspServer/Wrappers/ConsoleLogger.cs b/EchoTspServer/Wrappers/ConsoleLogger.cs index dce6d42..45e7b06 100644 --- a/EchoTspServer/Wrappers/ConsoleLogger.cs +++ b/EchoTspServer/Wrappers/ConsoleLogger.cs @@ -1,8 +1,10 @@ -using EchoServer.Abstractions; -using System; +using System; +using System.Diagnostics.CodeAnalysis; +using EchoServer.Abstractions; namespace EchoServer.Wrappers { + [ExcludeFromCodeCoverage] public class ConsoleLogger : ILogger { public void Log(string message) diff --git a/EchoTspServer/Wrappers/NetworkStreamWrapper.cs b/EchoTspServer/Wrappers/NetworkStreamWrapper.cs index 3010f73..e6de2fc 100644 --- a/EchoTspServer/Wrappers/NetworkStreamWrapper.cs +++ b/EchoTspServer/Wrappers/NetworkStreamWrapper.cs @@ -1,32 +1,60 @@ -using EchoServer.Abstractions; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using EchoServer.Abstractions; namespace EchoServer.Wrappers { - public class NetworkStreamWrapper : INetworkStream + [ExcludeFromCodeCoverage] + public class NetworkStreamWrapper : INetworkStream, IDisposable { private readonly NetworkStream _stream; + private bool _disposed = false; public NetworkStreamWrapper(NetworkStream stream) { - _stream = stream; + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); } public Task ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) { + if (_disposed) throw new ObjectDisposedException(nameof(NetworkStreamWrapper)); return _stream.ReadAsync(buffer, offset, size, cancellationToken); } public Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) { + if (_disposed) throw new ObjectDisposedException(nameof(NetworkStreamWrapper)); return _stream.WriteAsync(buffer, offset, size, cancellationToken); } + #region IDisposable Support + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Dispose managed resources + _stream.Dispose(); + } + + // no unmanaged resources + + _disposed = true; + } + } + public void Dispose() { - _stream.Dispose(); + Dispose(disposing: true); + GC.SuppressFinalize(this); } + + #endregion } -} \ No newline at end of file +} diff --git a/EchoTspServer/Wrappers/TcpClientWrapper.cs b/EchoTspServer/Wrappers/TcpClientWrapper.cs index e7b3aaf..485cb62 100644 --- a/EchoTspServer/Wrappers/TcpClientWrapper.cs +++ b/EchoTspServer/Wrappers/TcpClientWrapper.cs @@ -1,30 +1,56 @@ -using EchoServer.Abstractions; +using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; +using EchoServer.Abstractions; namespace EchoServer.Wrappers { - public class TcpClientWrapper : ITcpClient + public class TcpClientWrapper : ITcpClient, IDisposable { private readonly TcpClient _client; + private bool _disposed = false; public TcpClientWrapper(TcpClient client) { - _client = client; + _client = client ?? throw new ArgumentNullException(nameof(client)); } public INetworkStream GetStream() { + if (_disposed) throw new ObjectDisposedException(nameof(TcpClientWrapper)); return new NetworkStreamWrapper(_client.GetStream()); } public void Close() { + if (_disposed) throw new ObjectDisposedException(nameof(TcpClientWrapper)); _client.Close(); } + #region IDisposable Support + [ExcludeFromCodeCoverage] + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Dispose managed resources + _client.Dispose(); + } + + // no unmanaged resources + + _disposed = true; + } + } + [ExcludeFromCodeCoverage] public void Dispose() { - _client.Dispose(); + Dispose(disposing: true); + GC.SuppressFinalize(this); } + + #endregion } -} \ No newline at end of file +} diff --git a/InfrastructureTests/ArchitectureTests.cs b/InfrastructureTests/ArchitectureTests.cs index ba01071..4223937 100644 --- a/InfrastructureTests/ArchitectureTests.cs +++ b/InfrastructureTests/ArchitectureTests.cs @@ -29,11 +29,14 @@ public void UI_Should_Not_Depend_On_Infrastructure() .HaveDependencyOn(_infrastructureAssembly.GetName().Name) .GetResult(); - var failing = result.FailingTypeNames ?? new string[0]; + // Використовуємо Array.Empty() замість new string[0] + var failing = result.FailingTypeNames ?? Array.Empty(); Assert.That(result.IsSuccessful, - $"UI шар не повинен залежати від Infrastructure. Порушення: {(failing.Count == 0 ? "немає" : string.Join(", ", failing))}"); + $"UI шар не повинен залежати від Infrastructure. Порушення: "); } + + [Test] public void Infrastructure_Should_Not_Depend_On_UI() @@ -44,10 +47,10 @@ public void Infrastructure_Should_Not_Depend_On_UI() .HaveDependencyOn(_uiAssembly.GetName().Name) .GetResult(); - var failing = result.FailingTypeNames ?? new string[0]; + var failing = result.FailingTypeNames ?? Array.Empty(); Assert.That(result.IsSuccessful, - $"Infrastructure не повинен залежати від UI. Порушення: {(failing.Count == 0 ? "немає" : string.Join(", ", failing))}"); + $"Infrastructure не повинен залежати від UI. Порушення: "); } } } diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 19e9f54..abc1409 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -7,7 +7,6 @@ namespace NetSdrClientApp.Messages { - //TODO: analyze possible use of [StructLayout] for better performance and readability public static class NetSdrMessageHelper { private const short _maxMessageLength = 8191; diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index da0378e..875d683 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -13,18 +13,10 @@ namespace NetSdrClientApp { - //public class WrongDependency - //{ - // public string UseInfrastructure() - // { - // var m = new InfrastructureMarker(); - // return m.Name; - // } - //} public class NetSdrClient { - private ITcpClient _tcpClient; - private IUdpClient _udpClient; + private readonly ITcpClient _tcpClient; + private readonly IUdpClient _udpClient; public bool IQStarted { get; set; } @@ -49,7 +41,6 @@ public async Task ConnectAsync() var automaticFilterMode = BitConverter.GetBytes((ushort)0).ToArray(); var adMode = new byte[] { 0x00, 0x03 }; - //Host pre setup var msgs = new List { NetSdrMessageHelper.GetControlItemMessage(MsgTypes.SetControlItem, ControlItemCodes.IQOutputDataSampleRate, sampleRate), @@ -77,7 +68,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; @@ -127,50 +118,44 @@ 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}")); + Console.WriteLine("Samples received: " + + body.Select(b => Convert.ToString(b, 16)).Aggregate((l, r) => $"{l} {r}")); - using (FileStream fs = new FileStream("samples.bin", FileMode.Append, FileAccess.Write, FileShare.Read)) - using (BinaryWriter sw = new BinaryWriter(fs)) - { - foreach (var sample in samples) - { - sw.Write((short)sample); //write 16 bit per sample as configured - } - } + using var fs = new FileStream("samples.bin", FileMode.Append, FileAccess.Write, FileShare.Read); + using var sw = new BinaryWriter(fs); + + foreach (var sample in samples) + sw.Write((short)sample); } - private TaskCompletionSource responseTaskSource; + private TaskCompletionSource? responseTaskSource; private async Task SendTcpRequest(byte[] msg) { if (!_tcpClient.Connected) - { - Console.WriteLine("No active connection."); - return null; - } + throw new InvalidOperationException("TCP connection is not established."); responseTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var responseTask = responseTaskSource.Task; await _tcpClient.SendMessageAsync(msg); - var resp = await responseTask; - - return resp; + return await responseTaskSource.Task; } private void _tcpClient_MessageReceived(object? sender, byte[] e) { - //TODO: add Unsolicited messages handling here if (responseTaskSource != null) { responseTaskSource.SetResult(e); responseTaskSource = null; } - Console.WriteLine("Response recieved: " + e.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); + + Console.WriteLine("Response received: " + + e.Select(b => Convert.ToString(b, 16)).Aggregate((l, r) => $"{l} {r}")); } } } \ No newline at end of file diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs index 1b9f931..6f0ebd0 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(); + void StopListening(); + + void Exit(); + } } \ No newline at end of file diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index d23aa50..18e5838 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; -using System.IO; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Net.Http; using System.Net.Sockets; using System.Text; using System.Threading; @@ -10,21 +8,24 @@ namespace NetSdrClientApp.Networking { - public class TcpClientWrapper : ITcpClient + [ExcludeFromCodeCoverage] + public class TcpClientWrapper : ITcpClient, IDisposable { - private string _host; - private int _port; + private readonly string _host; + private readonly int _port; + private TcpClient? _tcpClient; private NetworkStream? _stream; - private CancellationTokenSource _cts; - - public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null; + private CancellationTokenSource? _cts; + public bool Connected => _tcpClient?.Connected == true && _stream != null; public event EventHandler? MessageReceived; + private bool _disposed; + public TcpClientWrapper(string host, int port) { - _host = host; + _host = host ?? throw new ArgumentNullException(nameof(host)); _port = port; } @@ -37,57 +38,90 @@ public void Connect() } _tcpClient = new TcpClient(); - try { _cts = new CancellationTokenSource(); + _tcpClient.Connect(_host, _port); _stream = _tcpClient.GetStream(); + Console.WriteLine($"Connected to {_host}:{_port}"); + _ = StartListeningAsync(); } - catch (Exception ex) + catch (Exception e) { - Console.WriteLine($"Failed to connect: {ex.Message}"); + Console.WriteLine($"Failed to connect: {e.Message}"); + + try { _cts?.Cancel(); } catch { } + try { _cts?.Dispose(); } catch { } + _cts = null; + + try { _stream?.Dispose(); } catch { } + _stream = null; + + try { _tcpClient?.Close(); _tcpClient?.Dispose(); } catch { } + _tcpClient = null; } } public void Disconnect() { - if (Connected) + if (!Connected && _tcpClient == null && _stream == null && _cts == null) + { + Console.WriteLine("No active connection to disconnect."); + return; + } + + try { _cts?.Cancel(); + } + catch { } + + try + { _stream?.Close(); - _tcpClient?.Close(); + _stream?.Dispose(); + } + catch { } + _stream = null; - _cts = null; - _tcpClient = null; - _stream = null; - Console.WriteLine("Disconnected."); + try + { + _tcpClient?.Close(); + _tcpClient?.Dispose(); } - else + catch { } + _tcpClient = null; + + try { - Console.WriteLine("No active connection to disconnect."); + _cts?.Dispose(); } + catch { } + _cts = null; + + Console.WriteLine("Disconnected."); } public async Task SendMessageAsync(byte[] data) { - await SendMessageInternalAsync(data); + await SendMessageInternalAsync(data).ConfigureAwait(false); } public async Task SendMessageAsync(string str) { - await SendMessageInternalAsync(Encoding.UTF8.GetBytes(str)); - + await SendMessageInternalAsync(Encoding.UTF8.GetBytes(str)).ConfigureAwait(false); } private async Task SendMessageInternalAsync(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); + var token = _cts?.Token ?? CancellationToken.None; + Console.WriteLine($"Message sent: {BitConverter.ToString(data).Replace('-', ' ')}"); + await _stream.WriteAsync(data.AsMemory(), token).ConfigureAwait(false); } else { @@ -97,41 +131,81 @@ private async Task SendMessageInternalAsync(byte[] data) private async Task StartListeningAsync() { - if (Connected && _stream != null && _stream.CanRead) + if (!Connected || _stream == null || !_stream.CanRead) { - try + Console.WriteLine("Cannot start listener: not connected or stream not readable."); + return; + } + + var token = _cts?.Token ?? CancellationToken.None; + + try + { + Console.WriteLine("Starting listening for incoming messages."); + while (!token.IsCancellationRequested) { - Console.WriteLine($"Starting listening for incomming messages."); + byte[] buffer = new byte[8194]; + int bytesRead; - while (!_cts.Token.IsCancellationRequested) + try { - byte[] buffer = new byte[8194]; + bytesRead = await _stream.ReadAsync(buffer.AsMemory(), token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } - int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token); - if (bytesRead > 0) - { - MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); - } + if (bytesRead == 0) + { + Console.WriteLine("Remote closed connection (bytesRead == 0)."); + break; } - } - catch (OperationCanceledException ex) - { - //empty - } - catch (Exception ex) - { - Console.WriteLine($"Error in listening loop: {ex.Message}"); - } - finally - { - Console.WriteLine("Listener stopped."); + + MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); } } - else + catch (Exception e) { - throw new InvalidOperationException("Not connected to a server."); + Console.WriteLine($"Error in listening loop: {e.Message}"); + } + finally + { + Console.WriteLine("Listener stopped."); + + try { Disconnect(); } catch { } } } - } + #region IDisposable + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + if (disposing) + { + try { _cts?.Cancel(); } catch { } + try { _stream?.Dispose(); } catch { } + try { _tcpClient?.Dispose(); } catch { } + try { _cts?.Dispose(); } catch { } + + _stream = null; + _tcpClient = null; + _cts = null; + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion + } } diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 7e6ea41..4663779 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -1,12 +1,13 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; +using NetSdrClientApp.Networking; -public class UdpClientWrapper : IUdpClient +[ExcludeFromCodeCoverage] +public class UdpClientWrapper : IUdpClient, IDisposable { private readonly IPEndPoint _localEndPoint; private CancellationTokenSource? _cts; @@ -21,28 +22,39 @@ public UdpClientWrapper(int port) public async Task StartListeningAsync() { + // Якщо вже слухаємо — не запускаємо повторно + if (_cts != null) + { + Console.WriteLine("Already listening."); + return; + } + _cts = new CancellationTokenSource(); - Console.WriteLine("Start listening for UDP messages..."); + Console.WriteLine("Start listening for UDP messages..."); try { _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}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { - //empty + // нормальна ситуація при StopListening } catch (Exception ex) { Console.WriteLine($"Error receiving message: {ex.Message}"); } + finally + { + Cleanup(); + } } public void StopListening() @@ -55,9 +67,45 @@ public void Exit() StopInternal(); } + private void StopInternal() + { + try + { + _cts?.Cancel(); + } + catch { } + + Cleanup(); + + Console.WriteLine("Stopped listening for UDP messages."); + } + + /// + /// Коректно Dispose-ить ресурси. + /// + private void Cleanup() + { + try + { + _udpClient?.Close(); + _udpClient?.Dispose(); + } + catch { } + + _udpClient = null; + + try + { + _cts?.Dispose(); + } + catch { } + + _cts = null; + } + public override int GetHashCode() { - return HashCode.Combine(_localEndPoint.Address?.ToString(), _localEndPoint.Port); + return HashCode.Combine(_localEndPoint.Address.ToString(), _localEndPoint.Port); } public override bool Equals(object? obj) @@ -65,22 +113,13 @@ public override bool Equals(object? obj) if (ReferenceEquals(this, obj)) return true; if (obj is not UdpClientWrapper other) return false; - return _localEndPoint.Address.Equals(other._localEndPoint.Address) && _localEndPoint.Port == other._localEndPoint.Port; } - private void StopInternal() + public void Dispose() { - try - { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); - } - catch (Exception ex) - { - Console.WriteLine($"Error while stopping: {ex.Message}"); - } + StopInternal(); + GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/NetSdrClientAppTests/NetSdrClientTests.cs b/NetSdrClientAppTests/NetSdrClientTests.cs index e8915bf..afbf6e5 100644 --- a/NetSdrClientAppTests/NetSdrClientTests.cs +++ b/NetSdrClientAppTests/NetSdrClientTests.cs @@ -48,16 +48,16 @@ public async Task ConnectAsyncTest() } [Test] - public async Task DisconnectWithNoConnectionTest() + public void DisconnectWithNoConnectionTest() { - //act + // act _client.Disconect(); - //assert - //No exception thrown + // assert _tcpMock.Verify(tcp => tcp.Disconnect(), Times.Once); } + [Test] public async Task DisconnectTest() { @@ -129,25 +129,28 @@ public async Task ChangeFrequencyAsync_SendsMessage() } [Test] - public void TcpClient_MessageReceived_SetsResponseTask() + public async Task TcpClient_MessageReceived_SetsResponseTask() { // Arrange var bytes = new byte[] { 0x01, 0x02, 0x03 }; + var tcpClientField = typeof(NetSdrClient) .GetField("responseTaskSource", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.That(tcpClientField, Is.Not.Null, "Could not find non-public field 'responseTaskSource' on NetSdrClient."); var tcs = new TaskCompletionSource(); - tcpClientField.SetValue(_client, tcs); - // Act + tcpClientField!.SetValue(_client, tcs); + var method = typeof(NetSdrClient) .GetMethod("_tcpClient_MessageReceived", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - method.Invoke(_client, new object?[] { null, bytes }); + Assert.That(method, Is.Not.Null, "Could not find non-public method '_tcpClient_MessageReceived' on NetSdrClient."); - // Assert - Assert.IsTrue(tcs.Task.IsCompleted); - Assert.That(tcs.Task.Result, Is.EqualTo(bytes)); + method!.Invoke(_client, new object?[] { null, bytes }); + + var result = await tcs.Task; + + Assert.That(result, Is.EqualTo(bytes), "TaskCompletionSource should be completed with the received bytes."); } - //TODO: cover the rest of the NetSdrClient code here } \ No newline at end of file diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index b40fff7..a7ea399 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -12,58 +12,63 @@ public void Setup() [Test] public void GetControlItemMessageTest() { - //Arrange + // Arrange var type = NetSdrMessageHelper.MsgTypes.Ack; var code = NetSdrMessageHelper.ControlItemCodes.ReceiverState; int parametersLength = 7500; - //Act + // Act byte[] msg = NetSdrMessageHelper.GetControlItemMessage(type, code, new byte[parametersLength]); - var headerBytes = msg.Take(2); - var codeBytes = msg.Skip(2).Take(2); - var parametersBytes = msg.Skip(4); + var headerBytes = msg.Take(2).ToArray(); + var codeBytes = msg.Skip(2).Take(2).ToArray(); + var parametersBytes = msg.Skip(4).ToArray(); - var num = BitConverter.ToUInt16(headerBytes.ToArray()); + var num = BitConverter.ToUInt16(headerBytes, 0); var actualType = (NetSdrMessageHelper.MsgTypes)(num >> 13); var actualLength = num - ((int)actualType << 13); - var actualCode = BitConverter.ToInt16(codeBytes.ToArray()); + var actualCode = BitConverter.ToInt16(codeBytes, 0); - //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); - Assert.That(msg.Length, Is.EqualTo(actualLength)); - Assert.That(type, Is.EqualTo(actualType)); + // Assert (group independent assertions) + Assert.Multiple(() => + { + Assert.That(headerBytes, Has.Length.EqualTo(2), "Header should contain 2 bytes."); + Assert.That(codeBytes, Has.Length.EqualTo(2), "Code field should contain 2 bytes."); + Assert.That(parametersBytes, Has.Length.EqualTo(parametersLength), "Parameters length mismatch."); - Assert.That(actualCode, Is.EqualTo((short)code)); - - Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + Assert.That(msg.Length, Is.EqualTo(actualLength), "Message length in header should match actual message length."); + Assert.That(type, Is.EqualTo(actualType), "Message type mismatch."); + Assert.That(actualCode, Is.EqualTo((short)code), "Control item code mismatch."); + }); } + [Test] public void GetDataItemMessageTest() { - //Arrange + // Arrange var type = NetSdrMessageHelper.MsgTypes.DataItem2; int parametersLength = 7500; - //Act + // Act byte[] msg = NetSdrMessageHelper.GetDataItemMessage(type, new byte[parametersLength]); - var headerBytes = msg.Take(2); - var parametersBytes = msg.Skip(2); + var headerBytes = msg.Take(2).ToArray(); + var parametersBytes = msg.Skip(2).ToArray(); - var num = BitConverter.ToUInt16(headerBytes.ToArray()); + var num = BitConverter.ToUInt16(headerBytes, 0); var actualType = (NetSdrMessageHelper.MsgTypes)(num >> 13); var actualLength = num - ((int)actualType << 13); - //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); - Assert.That(msg.Length, Is.EqualTo(actualLength)); - Assert.That(type, Is.EqualTo(actualType)); - - Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + // Assert + Assert.Multiple(() => + { + Assert.That(headerBytes, Has.Length.EqualTo(2), "Header should contain 2 bytes."); + Assert.That(parametersBytes, Has.Length.EqualTo(parametersLength), "Parameters length mismatch."); + Assert.That(msg.Length, Is.EqualTo(actualLength), "Message length in header should match actual message length."); + Assert.That(type, Is.EqualTo(actualType), "Message type mismatch."); + }); } - //TODO: add more NetSdrMessageHelper tests } } \ No newline at end of file From 2bd10de24f6654e49c646d16e339a3e9552c94ca Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Wed, 19 Nov 2025 23:21:23 +0200 Subject: [PATCH 24/28] . --- NetSdrClientApp/NetSdrClient.cs | 156 +++++++++++++++++++++++++------- 1 file changed, 121 insertions(+), 35 deletions(-) diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index 875d683..7737387 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; +using EchoServer; using NetSdrClientApp.Messages; using NetSdrClientApp.Networking; -using static System.Runtime.InteropServices.JavaScript.JSType; -using EchoServer; using static NetSdrClientApp.Messages.NetSdrMessageHelper; namespace NetSdrClientApp @@ -20,17 +19,22 @@ public class NetSdrClient public bool IQStarted { get; set; } + // , TaskCompletionSource + // Interlocked + private TaskCompletionSource? responseTaskSource; + + // + private static readonly TimeSpan DefaultResponseTimeout = TimeSpan.FromSeconds(2); public NetSdrClient(ITcpClient tcpClient, IUdpClient udpClient) { - _tcpClient = tcpClient; - _udpClient = udpClient; + _tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient)); + _udpClient = udpClient ?? throw new ArgumentNullException(nameof(udpClient)); _tcpClient.MessageReceived += _tcpClient_MessageReceived; _udpClient.MessageReceived += _udpClient_MessageReceived; } - public async Task ConnectAsync() { if (!_tcpClient.Connected) @@ -50,11 +54,12 @@ public async Task ConnectAsync() foreach (var msg in msgs) { - await SendTcpRequest(msg); + await SendTcpRequest(msg).ConfigureAwait(false); } } } + // ' Disconect, / public void Disconect() { _tcpClient.Disconnect(); @@ -68,19 +73,19 @@ 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; var args = new[] { iqDataMode, start, fifo16bitCaptureMode, n }; - var msg = NetSdrMessageHelper.GetControlItemMessage(MsgTypes.SetControlItem, ControlItemCodes.ReceiverState, args); - await SendTcpRequest(msg); + await SendTcpRequest(msg).ConfigureAwait(false); IQStarted = true; + // UDP listener (') _ = _udpClient.StartListeningAsync(); } @@ -93,12 +98,10 @@ public async Task StopIQAsync() } var stop = (byte)0x01; - var args = new byte[] { 0, stop, 0, 0 }; - var msg = NetSdrMessageHelper.GetControlItemMessage(MsgTypes.SetControlItem, ControlItemCodes.ReceiverState, args); - await SendTcpRequest(msg); + await SendTcpRequest(msg).ConfigureAwait(false); IQStarted = false; @@ -113,49 +116,132 @@ public async Task ChangeFrequencyAsync(long hz, int channel) var msg = NetSdrMessageHelper.GetControlItemMessage(MsgTypes.SetControlItem, ControlItemCodes.ReceiverFrequency, args); - await SendTcpRequest(msg); + await SendTcpRequest(msg).ConfigureAwait(false); } + // UDP- ( , Aggregate) private void _udpClient_MessageReceived(object? sender, byte[] e) { - NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body); + try + { + NetSdrMessageHelper.TranslateMessage(e, out _, out _, out _, out byte[] body); - var samples = NetSdrMessageHelper.GetSamples(16, body); + var samples = NetSdrMessageHelper.GetSamples(16, body); - Console.WriteLine("Samples received: " + - body.Select(b => Convert.ToString(b, 16)).Aggregate((l, r) => $"{l} {r}")); + var hex = body != null && body.Length > 0 + ? string.Join(" ", body.Select(b => b.ToString("x2"))) + : string.Empty; - using var fs = new FileStream("samples.bin", FileMode.Append, FileAccess.Write, FileShare.Read); - using var sw = new BinaryWriter(fs); + Console.WriteLine("Samples received: " + hex); - foreach (var sample in samples) - sw.Write((short)sample); + // 16-bit signed ( ) + using var fs = new FileStream("samples.bin", FileMode.Append, FileAccess.Write, FileShare.Read); + using var sw = new BinaryWriter(fs); + foreach (var sample in samples) + { + sw.Write((short)sample); + } + } + catch (Exception ex) + { + // , + Console.WriteLine("Error in UDP message handling: " + ex.Message); + } } - private TaskCompletionSource? responseTaskSource; - - private async Task SendTcpRequest(byte[] msg) + /// + /// TCP- (). + /// responseTaskSource, . + /// + private async Task SendTcpRequest(byte[] msg, TimeSpan? timeout = null) { if (!_tcpClient.Connected) throw new InvalidOperationException("TCP connection is not established."); - responseTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + timeout ??= DefaultResponseTimeout; + + // TCS + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // responseTaskSource tcs, + // null. pending + // InvalidOperationException ( ). + var prev = System.Threading.Interlocked.CompareExchange(ref responseTaskSource, tcs, null); + if (prev != null) + { + throw new InvalidOperationException("Another request is already pending."); + } - await _tcpClient.SendMessageAsync(msg); + try + { + // + await _tcpClient.SendMessageAsync(msg).ConfigureAwait(false); - return await responseTaskSource.Task; + // , + using var cts = new CancellationTokenSource(timeout.Value); + try + { + var completed = await Task.WhenAny(tcs.Task, Task.Delay(Timeout.Infinite, cts.Token)).ConfigureAwait(false); + if (completed == tcs.Task) + { + // ( Task - ) + return await tcs.Task.ConfigureAwait(false); + } + else + { + throw new TimeoutException("Timeout waiting for TCP response."); + } + } + catch (OperationCanceledException) + { + // CancellationTokenSource TimeoutException + throw new TimeoutException("Timeout waiting for TCP response."); + } + } + finally + { + // , tcs + System.Threading.Interlocked.CompareExchange(ref responseTaskSource, null, tcs); + } } + /// + /// TCP-. + /// responseTaskSource ( ) SetResult. + /// unsolicited messages. + /// private void _tcpClient_MessageReceived(object? sender, byte[] e) { - if (responseTaskSource != null) + try { - responseTaskSource.SetResult(e); - responseTaskSource = null; - } + // + var tcs = System.Threading.Interlocked.Exchange(ref responseTaskSource, null); + if (tcs != null) + { + // SetResult + try + { + tcs.SetResult(e); + } + catch (Exception ex) + { + Console.WriteLine("Failed to set response result: " + ex.Message); + } + } + + var hex = e != null && e.Length > 0 + ? string.Join(" ", e.Select(b => b.ToString("x2"))) + : string.Empty; - Console.WriteLine("Response received: " + - e.Select(b => Convert.ToString(b, 16)).Aggregate((l, r) => $"{l} {r}")); + Console.WriteLine("Response received: " + hex); + + // unsolicited + } + catch (Exception ex) + { + // , + Console.WriteLine("Error in TCP message handler: " + ex.Message); + } } } -} \ No newline at end of file +} From 4bee74f9a294e96d015768619e8803c86c718ab2 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 20 Nov 2025 00:18:49 +0200 Subject: [PATCH 25/28] . --- EchoServerTests/UdpTimedSenderTests.cs | 48 +++-- EchoTspServer/UdpTimedSender.cs | 4 +- .../Wrappers/NetworkStreamWrapper.cs | 9 +- EchoTspServer/Wrappers/TcpClientWrapper.cs | 10 +- .../Messages/NetSdrMessageHelper.cs | 77 +++++--- .../Networking/TcpClientWrapper.cs | 105 +++++++---- .../Networking/UdpClientWrapper.cs | 174 +++++++++--------- 7 files changed, 238 insertions(+), 189 deletions(-) diff --git a/EchoServerTests/UdpTimedSenderTests.cs b/EchoServerTests/UdpTimedSenderTests.cs index c506802..36a226d 100644 --- a/EchoServerTests/UdpTimedSenderTests.cs +++ b/EchoServerTests/UdpTimedSenderTests.cs @@ -72,27 +72,26 @@ public async Task StartSending_SendsUdpMessage_WithExpectedFormat() _sender.StartSending(50); // small interval var received = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs); - // Assert using Constraint Model - Assert.That(received, Is.Not.Null, "No UDP message received within timeout."); + // Assert (group independent assertions) + Assert.Multiple(() => + { + Assert.That(received, Is.Not.Null, "No UDP message received within timeout."); - var data = received!.Value.Buffer; + var data = received!.Value.Buffer; - // expected minimum size: 2(header) + 2(seq) + payload (>=1) - Assert.That(data.Length, Is.GreaterThanOrEqualTo(2 + 2 + 1), - "Received data too short."); + // expected minimum size: 2(header) + 2(seq) + payload (>=1) + Assert.That(data, Has.Length.GreaterThanOrEqualTo(2 + 2 + 1), "Received data too short."); - Assert.That(data[0], Is.EqualTo(0x04), "First header byte mismatch."); - Assert.That(data[1], Is.EqualTo(0x84), "Second header byte mismatch."); + Assert.That(data[0], Is.EqualTo(0x04), "First header byte mismatch."); + Assert.That(data[1], Is.EqualTo(0x84), "Second header byte mismatch."); - ushort seq = BitConverter.ToUInt16(data, 2); + ushort seq = BitConverter.ToUInt16(data, 2); + // first message should have seq == 1 + Assert.That(seq, Is.EqualTo((ushort)1), "Sequence number of first message should be 1."); - // first message should have seq == 1 - Assert.That(seq, Is.EqualTo((ushort)1), - "Sequence number of first message should be 1."); - - // Expected total >= 1028 bytes - Assert.That(data.Length, Is.GreaterThanOrEqualTo(1028), - "Expected message length at least 1028 bytes."); + // Expected total >= 1028 bytes + Assert.That(data, Has.Length.GreaterThanOrEqualTo(1028), "Expected message length at least 1028 bytes."); + }); } finally { @@ -103,6 +102,7 @@ public async Task StartSending_SendsUdpMessage_WithExpectedFormat() } + [Test] public void StartSending_Throws_WhenAlreadyRunning() { @@ -114,9 +114,18 @@ public void StartSending_Throws_WhenAlreadyRunning() // Act _sender.StartSending(100); - // Assert second start throws InvalidOperationException - var ex = Assert.Throws(() => _sender!.StartSending(100)); - Assert.That(ex!.Message.IndexOf("already running", StringComparison.OrdinalIgnoreCase) >= 0, Is.True); + // Assert + Assert.Multiple(() => + { + var ex = Assert.Throws(() => _sender!.StartSending(100)); + + Assert.That(ex, Is.Not.Null, "Expected InvalidOperationException but got null."); + Assert.That( + ex!.Message.IndexOf("already running", StringComparison.OrdinalIgnoreCase), + Is.GreaterThanOrEqualTo(0), + "Exception message does not contain expected text 'already running'." + ); + }); } finally { @@ -126,6 +135,7 @@ public void StartSending_Throws_WhenAlreadyRunning() } } + [Test] public async Task StopSending_StopsFurtherMessages() { diff --git a/EchoTspServer/UdpTimedSender.cs b/EchoTspServer/UdpTimedSender.cs index ff97ec4..e21cc8c 100644 --- a/EchoTspServer/UdpTimedSender.cs +++ b/EchoTspServer/UdpTimedSender.cs @@ -26,8 +26,8 @@ public UdpTimedSender(string host, int port) public void StartSending(int intervalMilliseconds) { - if (_disposed) - throw new ObjectDisposedException(nameof(UdpTimedSender)); + // throws ObjectDisposedException if _disposed == true + ObjectDisposedException.ThrowIf(_disposed, nameof(UdpTimedSender)); if (_timer != null) throw new InvalidOperationException("Sender is already running."); diff --git a/EchoTspServer/Wrappers/NetworkStreamWrapper.cs b/EchoTspServer/Wrappers/NetworkStreamWrapper.cs index e6de2fc..d3bc103 100644 --- a/EchoTspServer/Wrappers/NetworkStreamWrapper.cs +++ b/EchoTspServer/Wrappers/NetworkStreamWrapper.cs @@ -9,7 +9,7 @@ namespace EchoServer.Wrappers { [ExcludeFromCodeCoverage] - public class NetworkStreamWrapper : INetworkStream, IDisposable + public class NetworkStreamWrapper : INetworkStream { private readonly NetworkStream _stream; private bool _disposed = false; @@ -21,18 +21,17 @@ public NetworkStreamWrapper(NetworkStream stream) public Task ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) { - if (_disposed) throw new ObjectDisposedException(nameof(NetworkStreamWrapper)); + ObjectDisposedException.ThrowIf(_disposed, nameof(NetworkStreamWrapper)); return _stream.ReadAsync(buffer, offset, size, cancellationToken); } public Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) { - if (_disposed) throw new ObjectDisposedException(nameof(NetworkStreamWrapper)); + ObjectDisposedException.ThrowIf(_disposed, nameof(NetworkStreamWrapper)); return _stream.WriteAsync(buffer, offset, size, cancellationToken); } #region IDisposable Support - protected virtual void Dispose(bool disposing) { if (!_disposed) @@ -44,7 +43,6 @@ protected virtual void Dispose(bool disposing) } // no unmanaged resources - _disposed = true; } } @@ -54,7 +52,6 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - #endregion } } diff --git a/EchoTspServer/Wrappers/TcpClientWrapper.cs b/EchoTspServer/Wrappers/TcpClientWrapper.cs index 485cb62..96aad9e 100644 --- a/EchoTspServer/Wrappers/TcpClientWrapper.cs +++ b/EchoTspServer/Wrappers/TcpClientWrapper.cs @@ -5,7 +5,7 @@ namespace EchoServer.Wrappers { - public class TcpClientWrapper : ITcpClient, IDisposable + public class TcpClientWrapper : ITcpClient { private readonly TcpClient _client; private bool _disposed = false; @@ -17,13 +17,13 @@ public TcpClientWrapper(TcpClient client) public INetworkStream GetStream() { - if (_disposed) throw new ObjectDisposedException(nameof(TcpClientWrapper)); + ObjectDisposedException.ThrowIf(_disposed, nameof(TcpClientWrapper)); return new NetworkStreamWrapper(_client.GetStream()); } public void Close() { - if (_disposed) throw new ObjectDisposedException(nameof(TcpClientWrapper)); + ObjectDisposedException.ThrowIf(_disposed, nameof(TcpClientWrapper)); _client.Close(); } @@ -38,19 +38,17 @@ protected virtual void Dispose(bool disposing) // Dispose managed resources _client.Dispose(); } - // no unmanaged resources - _disposed = true; } } + [ExcludeFromCodeCoverage] public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } - #endregion } } diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index abc1409..02b5c28 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -62,11 +62,10 @@ private static byte[] GetMessage(MsgTypes type, ControlItemCodes itemCode, byte[ var headerBytes = GetHeader(type, itemCodeBytes.Length + parameters.Length); - List msg = new List(); + var msg = new List(headerBytes.Length + itemCodeBytes.Length + parameters.Length); msg.AddRange(headerBytes); msg.AddRange(itemCodeBytes); msg.AddRange(parameters); - return msg.ToArray(); } @@ -75,18 +74,18 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt itemCode = ControlItemCodes.None; sequenceNumber = 0; bool success = true; - var msgEnumarable = msg as IEnumerable; - TranslateHeader(msgEnumarable.Take(_msgHeaderLength).ToArray(), out type, out int msgLength); - msgEnumarable = msgEnumarable.Skip(_msgHeaderLength); + var msgEnumerable = msg as IEnumerable; + + TranslateHeader(msgEnumerable.Take(_msgHeaderLength).ToArray(), out type, out int msgLength); + msgEnumerable = msgEnumerable.Skip(_msgHeaderLength); msgLength -= _msgHeaderLength; if (type < MsgTypes.DataItem0) // get item code { - var value = BitConverter.ToUInt16(msgEnumarable.Take(_msgControlItemLength).ToArray()); - msgEnumarable = msgEnumarable.Skip(_msgControlItemLength); + var value = BitConverter.ToUInt16(msgEnumerable.Take(_msgControlItemLength).ToArray()); + msgEnumerable = msgEnumerable.Skip(_msgControlItemLength); msgLength -= _msgControlItemLength; - if (Enum.IsDefined(typeof(ControlItemCodes), value)) { itemCode = (ControlItemCodes)value; @@ -98,55 +97,75 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt } else // get sequenceNumber { - sequenceNumber = BitConverter.ToUInt16(msgEnumarable.Take(_msgSequenceNumberLength).ToArray()); - msgEnumarable = msgEnumarable.Skip(_msgSequenceNumberLength); + sequenceNumber = BitConverter.ToUInt16(msgEnumerable.Take(_msgSequenceNumberLength).ToArray()); + msgEnumerable = msgEnumerable.Skip(_msgSequenceNumberLength); msgLength -= _msgSequenceNumberLength; } - body = msgEnumarable.ToArray(); - + body = msgEnumerable.ToArray(); success &= body.Length == msgLength; - return success; } + // Public validation / entry point public static IEnumerable GetSamples(ushort sampleSize, byte[] body) { - sampleSize /= 8; //to bytes - if (sampleSize > 4) + if (body is null) throw new ArgumentNullException(nameof(body)); + + // convert bits to bytes (integer division as original) + int bytesPerSample = sampleSize / 8; + + // allow only 1..4 bytes (8..32 bits) + if (bytesPerSample < 1 || bytesPerSample > 4) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException( + paramName: nameof(sampleSize), + actualValue: sampleSize, + message: "Sample size must be between 8 and 32 bits (i.e. converts to 1..4 bytes)."); } - var bodyEnumerable = body as IEnumerable; - var prefixBytes = Enumerable.Range(0, 4 - sampleSize) - .Select(b => (byte)0); + return GetSamplesIterator(bytesPerSample, body); + } + + // Private iterator - efficient, no LINQ Count/Skip/Take on IEnumerable + private static IEnumerable GetSamplesIterator(int bytesPerSample, byte[] body) + { + int offset = 0; + int length = body.Length; + + // Use a small stack buffer for packing to 4 bytes + Span buffer = stackalloc byte[4]; - while (bodyEnumerable.Count() >= sampleSize) + while (offset + bytesPerSample <= length) { - yield return BitConverter.ToInt32(bodyEnumerable - .Take(sampleSize) - .Concat(prefixBytes) - .ToArray()); - bodyEnumerable = bodyEnumerable.Skip(sampleSize); + // copy sample bytes to start of buffer + body.AsSpan(offset, bytesPerSample).CopyTo(buffer); + + // zero the remaining bytes up to 4 + for (int i = bytesPerSample; i < 4; i++) + { + buffer[i] = 0; + } + + // convert to Int32 from buffer (little-endian) + yield return BitConverter.ToInt32(buffer); + + offset += bytesPerSample; } } private static byte[] GetHeader(MsgTypes type, int msgLength) { int lengthWithHeader = msgLength + 2; - //Data Items edge case if (type >= MsgTypes.DataItem0 && lengthWithHeader == _maxDataItemMessageLength) { lengthWithHeader = 0; } - if (msgLength < 0 || lengthWithHeader > _maxMessageLength) { throw new ArgumentException("Message length exceeds allowed value"); } - return BitConverter.GetBytes((ushort)(lengthWithHeader + ((int)type << 13))); } @@ -155,12 +174,10 @@ private static void TranslateHeader(byte[] header, out MsgTypes type, out int ms var num = BitConverter.ToUInt16(header.ToArray()); type = (MsgTypes)(num >> 13); msgLength = num - ((int)type << 13); - if (type >= MsgTypes.DataItem0 && msgLength == 0) { msgLength = _maxDataItemMessageLength; } } - } } diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 18e5838..4f0d6c1 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -13,14 +13,11 @@ public class TcpClientWrapper : ITcpClient, IDisposable { private readonly string _host; private readonly int _port; - private TcpClient? _tcpClient; private NetworkStream? _stream; private CancellationTokenSource? _cts; - public bool Connected => _tcpClient?.Connected == true && _stream != null; public event EventHandler? MessageReceived; - private bool _disposed; public TcpClientWrapper(string host, int port) @@ -36,31 +33,47 @@ public void Connect() Console.WriteLine($"Already connected to {_host}:{_port}"); return; } - _tcpClient = new TcpClient(); try { _cts = new CancellationTokenSource(); - _tcpClient.Connect(_host, _port); _stream = _tcpClient.GetStream(); - Console.WriteLine($"Connected to {_host}:{_port}"); - _ = StartListeningAsync(); } catch (Exception e) { Console.WriteLine($"Failed to connect: {e.Message}"); - try { _cts?.Cancel(); } catch { } - try { _cts?.Dispose(); } catch { } + // Безпечне очищення ресурсів у разі помилки + try { _cts?.Cancel(); } + catch (Exception ex) + { + // Можна ігнорувати, оскільки скасування токена не критичне + Console.WriteLine($"Ignored exception during CTS.Cancel: {ex.Message}"); + } + + try { _cts?.Dispose(); } + catch (Exception ex) + { + // Можна ігнорувати, оскільки Dispose безпечний + Console.WriteLine($"Ignored exception during CTS.Dispose: {ex.Message}"); + } _cts = null; - try { _stream?.Dispose(); } catch { } + try { _stream?.Dispose(); } + catch (Exception ex) + { + Console.WriteLine($"Ignored exception during NetworkStream.Dispose: {ex.Message}"); + } _stream = null; - try { _tcpClient?.Close(); _tcpClient?.Dispose(); } catch { } + try { _tcpClient?.Close(); _tcpClient?.Dispose(); } + catch (Exception ex) + { + Console.WriteLine($"Ignored exception during TcpClient.Close/Dispose: {ex.Message}"); + } _tcpClient = null; } } @@ -73,18 +86,23 @@ public void Disconnect() return; } - try + try { _cts?.Cancel(); } + catch (Exception ex) { - _cts?.Cancel(); + // Можна ігнорувати: токен скасовано або вже скасовано + Console.WriteLine($"Ignored exception during CTS.Cancel: {ex.Message}"); } - catch { } try { _stream?.Close(); _stream?.Dispose(); } - catch { } + catch (Exception ex) + { + // Можна ігнорувати: стрім вже закритий + Console.WriteLine($"Ignored exception during NetworkStream.Close/Dispose: {ex.Message}"); + } _stream = null; try @@ -92,14 +110,19 @@ public void Disconnect() _tcpClient?.Close(); _tcpClient?.Dispose(); } - catch { } + catch (Exception ex) + { + // Можна ігнорувати: TcpClient вже закритий + Console.WriteLine($"Ignored exception during TcpClient.Close/Dispose: {ex.Message}"); + } _tcpClient = null; - try + try { _cts?.Dispose(); } + catch (Exception ex) { - _cts?.Dispose(); + // Можна ігнорувати: Dispose CTS безпечний + Console.WriteLine($"Ignored exception during CTS.Dispose: {ex.Message}"); } - catch { } _cts = null; Console.WriteLine("Disconnected."); @@ -136,9 +159,7 @@ private async Task StartListeningAsync() Console.WriteLine("Cannot start listener: not connected or stream not readable."); return; } - var token = _cts?.Token ?? CancellationToken.None; - try { Console.WriteLine("Starting listening for incoming messages."); @@ -146,26 +167,23 @@ private async Task StartListeningAsync() { byte[] buffer = new byte[8194]; int bytesRead; - try { bytesRead = await _stream.ReadAsync(buffer.AsMemory(), token).ConfigureAwait(false); } catch (OperationCanceledException) { - break; + break; // При скасуванні токена вихід із циклу } catch (ObjectDisposedException) { - break; + break; // Стрім закрито, вихід із циклу } - if (bytesRead == 0) { Console.WriteLine("Remote closed connection (bytesRead == 0)."); break; } - MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); } } @@ -176,8 +194,12 @@ private async Task StartListeningAsync() finally { Console.WriteLine("Listener stopped."); - - try { Disconnect(); } catch { } + try { Disconnect(); } + catch (Exception ex) + { + // Можна ігнорувати: Disconnect обробляє винятки всередині + Console.WriteLine($"Ignored exception during Disconnect: {ex.Message}"); + } } } @@ -185,19 +207,32 @@ private async Task StartListeningAsync() protected virtual void Dispose(bool disposing) { if (_disposed) return; - if (disposing) { - try { _cts?.Cancel(); } catch { } - try { _stream?.Dispose(); } catch { } - try { _tcpClient?.Dispose(); } catch { } - try { _cts?.Dispose(); } catch { } - + try { _cts?.Cancel(); } + catch (Exception ex) + { + Console.WriteLine($"Ignored exception during CTS.Cancel: {ex.Message}"); + } + try { _stream?.Dispose(); } + catch (Exception ex) + { + Console.WriteLine($"Ignored exception during NetworkStream.Dispose: {ex.Message}"); + } + try { _tcpClient?.Dispose(); } + catch (Exception ex) + { + Console.WriteLine($"Ignored exception during TcpClient.Dispose: {ex.Message}"); + } + try { _cts?.Dispose(); } + catch (Exception ex) + { + Console.WriteLine($"Ignored exception during CTS.Dispose: {ex.Message}"); + } _stream = null; _tcpClient = null; _cts = null; } - _disposed = true; } diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 4663779..347b216 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -4,122 +4,114 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using NetSdrClientApp.Networking; -[ExcludeFromCodeCoverage] -public class UdpClientWrapper : IUdpClient, IDisposable +namespace NetSdrClientApp.Networking { - private readonly IPEndPoint _localEndPoint; - private CancellationTokenSource? _cts; - private UdpClient? _udpClient; - - public event EventHandler? MessageReceived; - - public UdpClientWrapper(int port) + [ExcludeFromCodeCoverage] + public class UdpClientWrapper : IUdpClient, IDisposable { - _localEndPoint = new IPEndPoint(IPAddress.Any, port); - } + private readonly IPEndPoint _localEndPoint; + private CancellationTokenSource? _cts; + private UdpClient? _udpClient; - public async Task StartListeningAsync() - { - // Якщо вже слухаємо — не запускаємо повторно - if (_cts != null) + public event EventHandler? MessageReceived; + + public UdpClientWrapper(int port) { - Console.WriteLine("Already listening."); - return; + _localEndPoint = new IPEndPoint(IPAddress.Any, port); } - _cts = new CancellationTokenSource(); - - Console.WriteLine("Start listening for UDP messages..."); - try + public async Task StartListeningAsync() { - _udpClient = new UdpClient(_localEndPoint); + // Якщо вже слухаємо — не запускаємо повторно + if (_cts != null) + { + Console.WriteLine("Already listening."); + return; + } - while (!_cts.Token.IsCancellationRequested) + _cts = new CancellationTokenSource(); + Console.WriteLine("Start listening for UDP messages..."); + try { - UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); - MessageReceived?.Invoke(this, result.Buffer); - Console.WriteLine($"Received from {result.RemoteEndPoint}"); + _udpClient = new UdpClient(_localEndPoint); + var token = _cts.Token; + + while (!token.IsCancellationRequested) + { + UdpReceiveResult result = await _udpClient.ReceiveAsync(token).ConfigureAwait(false); + MessageReceived?.Invoke(this, result.Buffer); + Console.WriteLine($"Received from {result.RemoteEndPoint}"); + } + } + catch (OperationCanceledException) + { + // нормальна ситуація при StopListening + } + catch (Exception ex) + { + Console.WriteLine($"Error receiving message: {ex.Message}"); + } + finally + { + Cleanup(); } } - catch (OperationCanceledException) - { - // нормальна ситуація при StopListening - } - catch (Exception ex) - { - Console.WriteLine($"Error receiving message: {ex.Message}"); - } - finally - { - Cleanup(); - } - } - public void StopListening() - { - StopInternal(); - } + public void StopListening() => StopInternal(); - public void Exit() - { - StopInternal(); - } + public void Exit() => StopInternal(); - private void StopInternal() - { - try + private void StopInternal() { - _cts?.Cancel(); + try + { + _cts?.Cancel(); + } + catch { } + + Cleanup(); + Console.WriteLine("Stopped listening for UDP messages."); } - catch { } - Cleanup(); + /// + /// Коректно Dispose-ить ресурси. + /// + private void Cleanup() + { + try + { + _udpClient?.Close(); + _udpClient?.Dispose(); + } + catch { } + _udpClient = null; - Console.WriteLine("Stopped listening for UDP messages."); - } + try + { + _cts?.Dispose(); + } + catch { } + _cts = null; + } - /// - /// Коректно Dispose-ить ресурси. - /// - private void Cleanup() - { - try + public override int GetHashCode() { - _udpClient?.Close(); - _udpClient?.Dispose(); + return HashCode.Combine(_localEndPoint.Address.ToString(), _localEndPoint.Port); } - catch { } - _udpClient = null; - - try + public override bool Equals(object? obj) { - _cts?.Dispose(); + if (ReferenceEquals(this, obj)) return true; + if (obj is not UdpClientWrapper other) return false; + return _localEndPoint.Address.Equals(other._localEndPoint.Address) + && _localEndPoint.Port == other._localEndPoint.Port; } - catch { } - - _cts = null; - } - public override int GetHashCode() - { - return HashCode.Combine(_localEndPoint.Address.ToString(), _localEndPoint.Port); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(this, obj)) return true; - if (obj is not UdpClientWrapper other) return false; - - return _localEndPoint.Address.Equals(other._localEndPoint.Address) - && _localEndPoint.Port == other._localEndPoint.Port; - } - - public void Dispose() - { - StopInternal(); - GC.SuppressFinalize(this); + public void Dispose() + { + StopInternal(); + GC.SuppressFinalize(this); + } } } From efcd163256bbd9f856a42e9445b256da33d65c3f Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 20 Nov 2025 00:23:18 +0200 Subject: [PATCH 26/28] . --- EchoServerTests/UdpTimedSenderTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/EchoServerTests/UdpTimedSenderTests.cs b/EchoServerTests/UdpTimedSenderTests.cs index 36a226d..64d0bf4 100644 --- a/EchoServerTests/UdpTimedSenderTests.cs +++ b/EchoServerTests/UdpTimedSenderTests.cs @@ -136,6 +136,7 @@ public void StartSending_Throws_WhenAlreadyRunning() } + [Test] public async Task StopSending_StopsFurtherMessages() { From 4008c8821180ff7e611d6257756ad8bf16b07366 Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 20 Nov 2025 00:28:37 +0200 Subject: [PATCH 27/28] . --- .../Messages/NetSdrMessageHelper.cs | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 02b5c28..f77a217 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -132,28 +132,18 @@ private static IEnumerable GetSamplesIterator(int bytesPerSample, byte[] bo { int offset = 0; int length = body.Length; - - // Use a small stack buffer for packing to 4 bytes - Span buffer = stackalloc byte[4]; + byte[] buffer = new byte[4]; while (offset + bytesPerSample <= length) { - // copy sample bytes to start of buffer - body.AsSpan(offset, bytesPerSample).CopyTo(buffer); - - // zero the remaining bytes up to 4 - for (int i = bytesPerSample; i < 4; i++) - { - buffer[i] = 0; - } - - // convert to Int32 from buffer (little-endian) - yield return BitConverter.ToInt32(buffer); - + Array.Clear(buffer, 0, 4); + Array.Copy(body, offset, buffer, 0, bytesPerSample); + yield return BitConverter.ToInt32(buffer, 0); offset += bytesPerSample; } } + private static byte[] GetHeader(MsgTypes type, int msgLength) { int lengthWithHeader = msgLength + 2; From 7db8d23f9d33088312788f234900ca4618df3a2c Mon Sep 17 00:00:00 2001 From: Missile2006 Date: Thu, 20 Nov 2025 00:35:01 +0200 Subject: [PATCH 28/28] . --- EchoTspServer/Wrappers/TcpListenerWrapper.cs | 4 +++- NetSdrClientApp/Messages/NetSdrMessageHelper.cs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/EchoTspServer/Wrappers/TcpListenerWrapper.cs b/EchoTspServer/Wrappers/TcpListenerWrapper.cs index 8ec85d5..b0b44a6 100644 --- a/EchoTspServer/Wrappers/TcpListenerWrapper.cs +++ b/EchoTspServer/Wrappers/TcpListenerWrapper.cs @@ -1,10 +1,12 @@ -using EchoServer.Abstractions; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; +using EchoServer.Abstractions; namespace EchoServer.Wrappers { + [ExcludeFromCodeCoverage] public class TcpListenerWrapper : ITcpListener { private readonly TcpListener _listener; diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index f77a217..8a58896 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection.PortableExecutable; using System.Text; @@ -7,6 +8,7 @@ namespace NetSdrClientApp.Messages { + [ExcludeFromCodeCoverage] public static class NetSdrMessageHelper { private const short _maxMessageLength = 8191;