Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b5ba927
Юніт-тести
Missile2006 Oct 16, 2025
52be089
Merge branch 'master' of https://github.com/Missile2006/NetSdrClient
Missile2006 Oct 16, 2025
15c5e84
.
Missile2006 Oct 16, 2025
ab80e13
.
Missile2006 Oct 16, 2025
bb0aac1
.
Missile2006 Oct 16, 2025
8ad16f1
.
Missile2006 Oct 16, 2025
a1c28b5
.
Missile2006 Oct 16, 2025
07581c2
Update sonarcloud.yml
Missile2006 Oct 16, 2025
83d33e8
.
Missile2006 Oct 16, 2025
56b0e63
Merge branch 'master' of https://github.com/Missile2006/NetSdrClient
Missile2006 Oct 16, 2025
132ffe0
Update sonarcloud.yml
Missile2006 Oct 16, 2025
d05f4af
Update README.md
Missile2006 Oct 16, 2025
93f93e4
.
Missile2006 Oct 16, 2025
c2dc284
Merge branch 'master' of https://github.com/Missile2006/NetSdrClient
Missile2006 Oct 16, 2025
7859e3f
.
Missile2006 Oct 16, 2025
d7fef11
Юніт-тести
Missile2006 Oct 16, 2025
0e00623
Зменшення дублікати коду
Missile2006 Oct 23, 2025
f2759b8
Update SonarCloud badges for Missile2006 project
Missile2006 Oct 23, 2025
65450e4
тести
Missile2006 Oct 23, 2025
8c81ff4
Merge branch 'master' of https://github.com/Missile2006/NetSdrClient
Missile2006 Oct 23, 2025
d471233
Update sonarcloud.yml
Missile2006 Oct 23, 2025
5f74d40
Update sonarcloud.yml
Missile2006 Oct 23, 2025
67731d2
.
Missile2006 Oct 23, 2025
e97eee8
Merge branch 'master' of https://github.com/Missile2006/NetSdrClient
Missile2006 Oct 23, 2025
2dd5eca
.
Missile2006 Oct 23, 2025
96d21d6
Рефакторінг коду EchoServer та додавання EchoServerTests
Missile2006 Nov 19, 2025
c204f00
Update coverage report paths in SonarCloud workflow
Missile2006 Nov 19, 2025
726f4fb
.
Missile2006 Nov 19, 2025
eada47c
Merge branch 'master' of https://github.com/Missile2006/NetSdrClient
Missile2006 Nov 19, 2025
80067c3
Fix indentation and shell declaration in sonarcloud.yml
Missile2006 Nov 19, 2025
57e8f39
Add Dependabot configuration for NuGet updates
Missile2006 Nov 19, 2025
ae6223b
Bump coverlet.collector from 6.0.0 to 6.0.4
dependabot[bot] Nov 19, 2025
7e53eca
Bump Microsoft.NET.Test.Sdk from 18.0.0 to 18.0.1
dependabot[bot] Nov 19, 2025
1a26bc3
Merge pull request #9 from Missile2006/dependabot/nuget/EchoServerTes…
Missile2006 Nov 19, 2025
92922d5
Bump NUnit.Analyzers from 3.9.0 to 4.11.2
dependabot[bot] Nov 19, 2025
19adf2a
Bump NUnit3TestAdapter from 4.5.0 to 5.2.0
dependabot[bot] Nov 19, 2025
b7b67ad
Bump SharpZipLib from 1.3.2 to 1.4.2
dependabot[bot] Nov 19, 2025
5312c4a
Bump NUnit from 3.14.0 to 4.4.0
dependabot[bot] Nov 19, 2025
cc0531c
Merge branch 'master' into dependabot/nuget/InfrastructureTests/multi…
Missile2006 Nov 19, 2025
8a44d24
Merge pull request #11 from Missile2006/dependabot/nuget/Infrastructu…
Missile2006 Nov 19, 2025
a11d2cb
Merge pull request #12 from Missile2006/dependabot/nuget/EchoServerTe…
Missile2006 Nov 19, 2025
e545fce
Merge branch 'master' into dependabot/nuget/EchoServerTests/multi-ac1…
Missile2006 Nov 19, 2025
7713df9
Merge pull request #13 from Missile2006/dependabot/nuget/EchoServerTe…
Missile2006 Nov 19, 2025
5174746
Merge branch 'master' into dependabot/nuget/EchoServerTests/multi-a6a…
Missile2006 Nov 19, 2025
39680ff
Merge pull request #14 from Missile2006/dependabot/nuget/EchoServerTe…
Missile2006 Nov 19, 2025
684170b
Merge pull request #15 from Missile2006/dependabot/nuget/NetSdrClient…
Missile2006 Nov 19, 2025
e2de9a2
Update coverage report paths in SonarCloud workflow
Missile2006 Nov 19, 2025
0ecd92a
Fix coverage collection in sonarcloud.yml
Missile2006 Nov 19, 2025
6e31f09
Add coverlet.msbuild package reference
Missile2006 Nov 19, 2025
1998b72
Fix coverage output paths in sonarcloud.yml
Missile2006 Nov 19, 2025
6866e72
.
Missile2006 Nov 19, 2025
f5e59b8
Merge branch 'master' of https://github.com/Missile2006/NetSdrClient
Missile2006 Nov 19, 2025
2bd10de
.
Missile2006 Nov 19, 2025
4bee74f
.
Missile2006 Nov 19, 2025
efcd163
.
Missile2006 Nov 19, 2025
4008c88
.
Missile2006 Nov 19, 2025
7db8d23
.
Missile2006 Nov 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "weekly"
20 changes: 15 additions & 5 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ jobs:
/k:"Missile2006_NetSdrClient" `
/o:"missile2006" `
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
/d:sonar.host.url="https://sonarcloud.io" `
/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 `
/d:sonar.qualitygate.wait=false
/d:sonar.qualitygate.wait=true
shell: pwsh


Expand All @@ -78,12 +78,22 @@ jobs:

- name: Tests with coverage (OpenCover)
run: |
dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build `
dotnet test EchoServerTests/EchoServerTests.csproj -c Release --no-build `
/p:CollectCoverage=true `
/p:CoverletOutput=TestResults/coverage.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
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 }}"
Expand Down
90 changes: 90 additions & 0 deletions EchoServerTests/EchoServerTests.cs
Original file line number Diff line number Diff line change
@@ -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<ITcpListener> _mockListener;
private Mock<ILogger> _mockLogger;
private EchoServer.EchoServer _server;

[SetUp]
public void Setup()
{
_mockListener = new Mock<ITcpListener>();
_mockLogger = new Mock<ILogger>();
_server = new EchoServer.EchoServer(_mockListener.Object, _mockLogger.Object);
}

[Test]
public async Task StartAsync_ShouldStartListenerAndAcceptClients()
{
var mockClient = new Mock<ITcpClient>();
var mockStream = new Mock<INetworkStream>();
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<ITcpClient>();
var mockStream = new Mock<INetworkStream>();
var exceptionMessage = "Connection was forcibly closed.";

mockClient.Setup(c => c.GetStream()).Returns(mockStream.Object);

mockStream.Setup(s => s.ReadAsync(It.IsAny<byte[]>(), 0, It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new IOException(exceptionMessage));

await _server.HandleClientAsync(mockClient.Object, CancellationToken.None);

mockStream.Verify(s => s.WriteAsync(It.IsAny<byte[]>(), 0, It.IsAny<int>(), It.IsAny<CancellationToken>()), 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);
}
}
}
30 changes: 30 additions & 0 deletions EchoServerTests/EchoServerTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="coverlet.msbuild" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EchoTspServer\EchoServer.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>

</Project>
220 changes: 220 additions & 0 deletions EchoServerTests/UdpTimedSenderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
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<UdpReceiveResult?> 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 (group independent assertions)
Assert.Multiple(() =>
{
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 (>=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.");

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.");

// Expected total >= 1028 bytes
Assert.That(data, Has.Length.GreaterThanOrEqualTo(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
Assert.Multiple(() =>
{
var ex = Assert.Throws<InvalidOperationException>(() => _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
{
_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.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.That(second, Is.Null, "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.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.That(afterDispose, Is.Null, "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.That(first, Is.Not.Null, "First message not received.");
var firstSeq = BitConverter.ToUInt16(first!.Value.Buffer, 2);

var second = await ReceiveWithTimeoutAsync(_listener!, ReceiveTimeoutMs);
Assert.That(second, Is.Not.Null, "Second message not received.");
var secondSeq = BitConverter.ToUInt16(second!.Value.Buffer, 2);

Assert.That(secondSeq, Is.EqualTo((ushort)(firstSeq + 1)), "Sequence should increment by 1.");
}
finally
{
_sender?.StopSending();
_sender?.Dispose();
_sender = null;
}
}
}
}
Loading
Loading