Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 34 additions & 46 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# This workflow helps you trigger a SonarCloud analysis of your code and populates
# GitHub Code Scanning alerts with the vulnerabilities found.
# Free for open source project.

# 1. Login to SonarCloud.io using your GitHub account

# 2. Import your project on SonarCloud
# * Add your GitHub organization first, then add your repository as a new project.
# * Please note that many languages are eligible for automatic analysis,
# which means that the analysis will start automatically without the need to set up GitHub Actions.
# * This behavior can be changed in Administration > Analysis Method.
# SonarCloud аналіз для NetSdrClient (.NET 8)
#
# 3. Follow the SonarCloud in-product tutorial
# * a. Copy/paste the Project Key and the Organization Key into the args parameter below
# (You'll find this information in SonarCloud. Click on "Information" at the bottom left)
#
# * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN
# (On SonarCloud, click on your avatar on top-right > My account > Security
# or go directly to https://sonarcloud.io/account/security/)

# Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/)
# or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9)
# Перед першим запуском обов'язково:
# 1. Створити проект у SonarCloud (Analyze new project) для цього репозиторію.
# 2. У SonarCloud вимкнути Automatic Analysis (Administration -> Analysis Method).
# 3. Згенерувати User Token у SonarCloud та додати його у GitHub Secrets форку
# під іменем SONAR_TOKEN (Repo Settings -> Secrets and variables -> Actions).
# 4. Замінити нижче змінні `SONAR_PROJECT_KEY` і `SONAR_ORGANIZATION` на власні
# значення з SonarCloud (вкладка Information знизу зліва у проєкті Sonar).

name: SonarCloud analysis

Expand All @@ -36,12 +18,16 @@ on:
workflow_dispatch:

permissions:
pull-requests: read # allows SonarCloud to decorate PRs with analysis results
pull-requests: read

env:
SONAR_PROJECT_KEY: nik-bykoff_ReengineeringCourse
SONAR_ORGANIZATION: nik-bykoff

jobs:
sonar-check:
name: Sonar Check
runs-on: windows-latest # безпечно для будь-яких .NET проектів
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
Expand All @@ -50,34 +36,36 @@ jobs:
with:
dotnet-version: '8.0.x'

# 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:"ppanchen_NetSdrClient" `
/o:"ppanchen" `
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
/d:sonar.cs.opencover.reportsPaths="**/coverage.xml" `
/d:sonar.cpd.cs.minimumTokens=40 `
/d:sonar.cpd.cs.minimumLines=5 `
/d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml `
/d:sonar.qualitygate.wait=true
/k:"${{ env.SONAR_PROJECT_KEY }}" `
/o:"${{ env.SONAR_ORGANIZATION }}" `
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
/d:sonar.cs.opencover.reportsPaths="**/coverage.xml" `
/d:sonar.cpd.cs.minimumTokens=40 `
/d:sonar.cpd.cs.minimumLines=5 `
/d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml `
/d:sonar.qualitygate.wait=true
shell: pwsh
# 2) BUILD & TEST

- name: Restore
run: dotnet restore NetSdrClient.sln

- name: Build
run: dotnet build NetSdrClient.sln -c Release --no-restore
#- name: Tests with coverage (OpenCover)
# run: |
# dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build `
# /p:CollectCoverage=true `
# /p:CoverletOutput=TestResults/coverage.xml `
# /p:CoverletOutputFormat=opencover
# shell: pwsh
# 3) END: SonarScanner

- name: Tests with coverage (OpenCover)
run: |
dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build `
/p:CollectCoverage=true `
/p:CoverletOutput=TestResults/coverage.xml `
/p:CoverletOutputFormat=opencover `
/p:Exclude="[NetSdrClientApp]NetSdrClientApp.Program"
shell: pwsh

- name: SonarScanner End
run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
shell: pwsh
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,9 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd

# Local environment / secrets
.env
.env.*
!.env.example
127 changes: 127 additions & 0 deletions EchoTcpServer.Tests/EchoCoreTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using EchoTcpServer;
using NUnit.Framework;

namespace EchoTcpServerTests
{
public class EchoCoreTests
{
[Test]
public async Task EchoLoopAsync_OnPlainPayload_WritesIdenticalBytesBack()
{
var inbound = Encoding.UTF8.GetBytes("hello world");
var input = new MemoryStream(inbound);
var output = new MemoryStream();
using var bridge = new BridgeStream(input, output);

long total = await EchoCore.EchoLoopAsync(bridge, CancellationToken.None);

Assert.Multiple(() =>
{
Assert.That(total, Is.EqualTo(inbound.Length));
Assert.That(output.ToArray(), Is.EqualTo(inbound));
});
}

[Test]
public async Task EchoLoopAsync_StopsWhenStreamClosed()
{
var input = new MemoryStream(Array.Empty<byte>());
var output = new MemoryStream();
using var bridge = new BridgeStream(input, output);

long total = await EchoCore.EchoLoopAsync(bridge, CancellationToken.None);

Assert.That(total, Is.EqualTo(0));
Assert.That(output.Length, Is.EqualTo(0));
}

[Test]
public void EchoLoopAsync_RespectsCancellationToken()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
var input = new MemoryStream(new byte[] { 1, 2, 3 });
var output = new MemoryStream();
using var bridge = new BridgeStream(input, output);

Assert.DoesNotThrowAsync(async () =>
{
await EchoCore.EchoLoopAsync(bridge, cts.Token);
});

Assert.That(output.Length, Is.EqualTo(0),
"Pre-cancelled token must short-circuit the loop before any echo happens.");
}

[Test]
public void EchoLoopAsync_NullStream_Throws()
{
Assert.ThrowsAsync<ArgumentNullException>(
async () => await EchoCore.EchoLoopAsync(null!, CancellationToken.None));
}

[Test]
public void EchoLoopAsync_NonPositiveBufferSize_Throws()
{
using var input = new MemoryStream();
using var output = new MemoryStream();
using var bridge = new BridgeStream(input, output);

Assert.ThrowsAsync<ArgumentOutOfRangeException>(
async () => await EchoCore.EchoLoopAsync(bridge, CancellationToken.None, bufferSize: 0));
}

/// <summary>
/// Couples a read-only input stream with a write-only output stream so the
/// echo loop can be exercised without real sockets.
/// </summary>
private sealed class BridgeStream : Stream
{
private readonly Stream _input;
private readonly Stream _output;

public BridgeStream(Stream input, Stream output)
{
_input = input;
_output = output;
}

public override bool CanRead => _input.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => _output.CanWrite;
public override long Length => throw new NotSupportedException();
public override long Position { get => 0; set => throw new NotSupportedException(); }

public override void Flush() => _output.Flush();

public override int Read(byte[] buffer, int offset, int count) =>
_input.Read(buffer, offset, count);

public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();

public override void SetLength(long value) =>
throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count) =>
_output.Write(buffer, offset, count);

protected override void Dispose(bool disposing)
{
if (disposing)
{
_input.Dispose();
_output.Dispose();
}

base.Dispose(disposing);
}
}
}
}
33 changes: 33 additions & 0 deletions EchoTcpServer.Tests/EchoTcpServer.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<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.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<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="4.5.0" />
</ItemGroup>

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

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

</Project>
64 changes: 64 additions & 0 deletions EchoTcpServer.Tests/UdpTimedSenderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using EchoTcpServer;
using NUnit.Framework;

namespace EchoTcpServerTests
{
public class UdpTimedSenderTests
{
[Test]
public void StartSending_TwiceWithoutStop_Throws()
{
using var sender = new UdpTimedSender("127.0.0.1", 60001);
sender.StartSending(60_000);

try
{
Assert.Throws<InvalidOperationException>(() => sender.StartSending(60_000));
}
finally
{
sender.StopSending();
}
}

[Test]
public void StopSending_AfterStart_AllowsSubsequentStart()
{
using var sender = new UdpTimedSender("127.0.0.1", 60002);

sender.StartSending(60_000);
sender.StopSending();

Assert.DoesNotThrow(() => sender.StartSending(60_000));
sender.StopSending();
}

[Test]
public void StartSending_WithNonPositiveInterval_ThrowsArgumentOutOfRange()
{
using var sender = new UdpTimedSender("127.0.0.1", 60003);
Assert.Throws<ArgumentOutOfRangeException>(() => sender.StartSending(0));
Assert.Throws<ArgumentOutOfRangeException>(() => sender.StartSending(-1));
}

[Test]
public void Constructor_NullHost_Throws()
{
Assert.Throws<ArgumentNullException>(() => new UdpTimedSender(null!, 60004));
}

[Test]
public void IsRunning_ReflectsTimerLifecycle()
{
using var sender = new UdpTimedSender("127.0.0.1", 60005);
Assert.That(sender.IsRunning, Is.False);

sender.StartSending(60_000);
Assert.That(sender.IsRunning, Is.True);

sender.StopSending();
Assert.That(sender.IsRunning, Is.False);
}
}
}
31 changes: 31 additions & 0 deletions EchoTcpServer/EchoCore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace EchoTcpServer
{
public static class EchoCore
{
public const int DefaultBufferSize = 8192;

public static async Task<long> EchoLoopAsync(Stream stream, CancellationToken token, int bufferSize = DefaultBufferSize)
{
if (stream is null) throw new ArgumentNullException(nameof(stream));
if (bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize));

byte[] buffer = new byte[bufferSize];
long total = 0;
int bytesRead;

while (!token.IsCancellationRequested
&& (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false)) > 0)
{
await stream.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false);
total += bytesRead;
}

return total;
}
}
}
Loading