build: bootstrap solution and quality gates (#3)
quality-gate / quality (push) Successful in 58s
quality-gate / quality (push) Successful in 58s
Closes #3
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{cs,csproj,props,targets}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
dotnet_sort_system_directives_first = true
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_style_namespace_declarations = file_scoped:warning
|
||||||
|
csharp_style_var_for_built_in_types = false:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||||
|
csharp_style_var_elsewhere = false:suggestion
|
||||||
|
|
||||||
|
[*.{json,yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: quality-gate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- codex/**
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install .NET SDK
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.301
|
||||||
|
|
||||||
|
- name: Restore locked dependencies
|
||||||
|
run: dotnet restore Rendezvous.slnx --locked-mode
|
||||||
|
|
||||||
|
- name: Verify formatting and analyzers
|
||||||
|
run: dotnet format Rendezvous.slnx --verify-no-changes --no-restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build Rendezvous.slnx --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test Rendezvous.slnx --configuration Release --no-build
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
TestResults/
|
||||||
|
.idea/
|
||||||
|
.vs/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<AnalysisLevel>latest-recommended</AnalysisLevel>
|
||||||
|
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
|
<RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="LiteNetLib" Version="2.1.4" />
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||||
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
<packageSourceMapping>
|
||||||
|
<packageSource key="nuget.org">
|
||||||
|
<package pattern="*" />
|
||||||
|
</packageSource>
|
||||||
|
</packageSourceMapping>
|
||||||
|
</configuration>
|
||||||
@@ -79,3 +79,21 @@ Rendezvous is currently in its initial design and bootstrap stage. The first imp
|
|||||||
|
|
||||||
The ratified v1 boundaries, trust decisions, privacy rules, safety budgets, and
|
The ratified v1 boundaries, trust decisions, privacy rules, safety budgets, and
|
||||||
threat model are indexed in [the architecture documentation](docs/architecture/README.md).
|
threat model are indexed in [the architecture documentation](docs/architecture/README.md).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The repository pins .NET SDK 10.0.301. From a clean clone, run the same gates as
|
||||||
|
CI from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet restore Rendezvous.slnx --locked-mode
|
||||||
|
dotnet format Rendezvous.slnx --verify-no-changes --no-restore
|
||||||
|
dotnet build Rendezvous.slnx --configuration Release --no-restore
|
||||||
|
dotnet test Rendezvous.slnx --configuration Release --no-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the bootstrap server with
|
||||||
|
`dotnet run --project src/FinalFactory.Rendezvous.Server`. It serves HTTP health endpoints and binds
|
||||||
|
the configured UDP mediator port; both stop through normal host cancellation.
|
||||||
|
The project dependency rules and supported runtime choices are documented in
|
||||||
|
[project and dependency boundaries](docs/architecture/project-boundaries.md).
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/FinalFactory.Rendezvous.Client/FinalFactory.Rendezvous.Client.csproj" />
|
||||||
|
<Project Path="src/FinalFactory.Rendezvous.Contracts/FinalFactory.Rendezvous.Contracts.csproj" />
|
||||||
|
<Project Path="src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Server.csproj" />
|
||||||
|
<Project Path="src/FinalFactory.Rendezvous.TestClient/FinalFactory.Rendezvous.TestClient.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/tests/">
|
||||||
|
<Project Path="tests/FinalFactory.Rendezvous.Tests/FinalFactory.Rendezvous.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Project and dependency boundaries
|
||||||
|
|
||||||
|
Tracking: #3
|
||||||
|
|
||||||
|
```text
|
||||||
|
FinalFactory.Rendezvous.Contracts <- FinalFactory.Rendezvous.Client
|
||||||
|
^ ^
|
||||||
|
| |
|
||||||
|
FinalFactory.Rendezvous.Server FinalFactory.Rendezvous.TestClient
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Contracts` targets `netstandard2.1` and contains only versioned,
|
||||||
|
transport-neutral IDs and wire contracts. It cannot reference Server,
|
||||||
|
LiteNetLib, or Godot.
|
||||||
|
- `Client` targets `netstandard2.1`, references Contracts and the pinned
|
||||||
|
LiteNetLib package, and contains no Godot or Server dependency.
|
||||||
|
- `Server` targets .NET 10 LTS, references Contracts and LiteNetLib, and owns
|
||||||
|
HTTP hosting, UDP mediation, application policy, and ephemeral state.
|
||||||
|
- `TestClient` targets .NET 8, references only the public Client/Contracts seams
|
||||||
|
and LiteNetLib, and must never reach into Server internals.
|
||||||
|
- `Tests` target .NET 10 and may reference every project solely to verify public
|
||||||
|
behavior and architecture boundaries.
|
||||||
|
|
||||||
|
The dependency-boundary tests inspect compiled assembly references. A forbidden
|
||||||
|
engine, transport, or server dependency therefore fails the normal test gate.
|
||||||
|
|
||||||
|
## Supported toolchain
|
||||||
|
|
||||||
|
- Build SDK: .NET SDK 10.0.301, pinned by `global.json`.
|
||||||
|
- Server runtime: .NET 10 LTS.
|
||||||
|
- Client/contracts compatibility target: .NET Standard 2.1, consumable by the
|
||||||
|
.NET 8-or-later runtime used by current Godot 4 C# projects.
|
||||||
|
- TestClient runtime: .NET 8.
|
||||||
|
- LiteNetLib: 2.1.4, pinned centrally and restored from the lock files.
|
||||||
|
|
||||||
|
The repository uses central package versions, per-project lock files,
|
||||||
|
deterministic compilation, nullable reference types, warnings as errors, current
|
||||||
|
.NET analyzers, and formatting verification. CI restores in locked mode so a
|
||||||
|
package graph change must be deliberate and committed.
|
||||||
|
|
||||||
|
Primary compatibility references:
|
||||||
|
|
||||||
|
- [.NET support policy](https://dotnet.microsoft.com/en-us/platform/support/policy)
|
||||||
|
- [Godot stable C# prerequisites](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_basics.html)
|
||||||
|
- [LiteNetLib 2.1.4 on NuGet](https://www.nuget.org/packages/LiteNetLib/2.1.4)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "10.0.301",
|
||||||
|
"rollForward": "disable",
|
||||||
|
"allowPrerelease": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<AssemblyName>FinalFactory.Rendezvous.Client</AssemblyName>
|
||||||
|
<RootNamespace>FinalFactory.Rendezvous.Client</RootNamespace>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
<PackageId>FinalFactory.Rendezvous.Client</PackageId>
|
||||||
|
<Description>Godot-independent client SDK for Final Factory Rendezvous.</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../FinalFactory.Rendezvous.Contracts/FinalFactory.Rendezvous.Contracts.csproj" />
|
||||||
|
<PackageReference Include="LiteNetLib" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
".NETStandard,Version=v2.1": {
|
||||||
|
"LiteNetLib": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.1.4, )",
|
||||||
|
"resolved": "2.1.4",
|
||||||
|
"contentHash": "KWlxvMw3Urpqj9joD96LRiK+LC62pQNs/zkXRJc+rHnxgkGp+vV703xzDrxRmv+V1YhCFfIGzs5nrVWtREIlyA=="
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.contracts": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<AssemblyName>FinalFactory.Rendezvous.Contracts</AssemblyName>
|
||||||
|
<RootNamespace>FinalFactory.Rendezvous.Contracts</RootNamespace>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
<PackageId>FinalFactory.Rendezvous.Contracts</PackageId>
|
||||||
|
<Description>Versioned transport-neutral contracts for Final Factory Rendezvous.</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
".NETStandard,Version=v2.1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<AssemblyName>FinalFactory.Rendezvous.Server</AssemblyName>
|
||||||
|
<RootNamespace>FinalFactory.Rendezvous.Server</RootNamespace>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../FinalFactory.Rendezvous.Contracts/FinalFactory.Rendezvous.Contracts.csproj" />
|
||||||
|
<PackageReference Include="LiteNetLib" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Net;
|
||||||
|
using FinalFactory.Rendezvous.Server.Transport;
|
||||||
|
|
||||||
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddOptions<UdpMediatorOptions>()
|
||||||
|
.BindConfiguration(UdpMediatorOptions.SectionName)
|
||||||
|
.ValidateDataAnnotations()
|
||||||
|
.Validate(
|
||||||
|
options => IPAddress.TryParse(options.ListenAddress, out _),
|
||||||
|
$"{UdpMediatorOptions.SectionName}:ListenAddress must be an IP address.")
|
||||||
|
.ValidateOnStart();
|
||||||
|
builder.Services.AddSingleton<UdpMediatorService>();
|
||||||
|
builder.Services.AddHostedService(static services => services.GetRequiredService<UdpMediatorService>());
|
||||||
|
|
||||||
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
|
app.MapGet("/health/live", static () => Results.Ok(new { status = "live" }));
|
||||||
|
app.MapGet(
|
||||||
|
"/health/ready",
|
||||||
|
static (UdpMediatorService mediator) => mediator.LocalEndpoint is null
|
||||||
|
? Results.StatusCode(StatusCodes.Status503ServiceUnavailable)
|
||||||
|
: Results.Ok(new { status = "ready" }));
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point marker used by integration-test hosts.
|
||||||
|
/// </summary>
|
||||||
|
public partial class Program;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace FinalFactory.Rendezvous.Server.Transport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the UDP endpoint reserved for the Rendezvous mediator.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UdpMediatorOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration section containing UDP mediator settings.
|
||||||
|
/// </summary>
|
||||||
|
public const string SectionName = "Rendezvous:Udp";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the numeric IP address to bind.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string ListenAddress { get; set; } = "0.0.0.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the UDP port. Zero requests an ephemeral port for tests.
|
||||||
|
/// </summary>
|
||||||
|
[Range(0, 65_535)]
|
||||||
|
public int Port { get; set; } = 9050;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FinalFactory.Rendezvous.Server.Transport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the cancellable UDP socket used by the future NAT mediator.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class UdpMediatorService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<UdpMediatorService> _logger;
|
||||||
|
private readonly UdpMediatorOptions _options;
|
||||||
|
private UdpClient? _udpClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new UDP mediator service.
|
||||||
|
/// </summary>
|
||||||
|
public UdpMediatorService(
|
||||||
|
IOptions<UdpMediatorOptions> options,
|
||||||
|
ILogger<UdpMediatorService> logger)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the bound endpoint after startup completes.
|
||||||
|
/// </summary>
|
||||||
|
public IPEndPoint? LocalEndpoint { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (_udpClient is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The UDP mediator is already running.");
|
||||||
|
}
|
||||||
|
|
||||||
|
IPAddress listenAddress = IPAddress.Parse(_options.ListenAddress);
|
||||||
|
UdpClient udpClient = new(new IPEndPoint(listenAddress, _options.Port));
|
||||||
|
_udpClient = udpClient;
|
||||||
|
IPEndPoint localEndpoint =
|
||||||
|
(IPEndPoint?)udpClient.Client.LocalEndPoint
|
||||||
|
?? throw new InvalidOperationException("The UDP socket did not expose its bound endpoint.");
|
||||||
|
LocalEndpoint = localEndpoint;
|
||||||
|
|
||||||
|
LogMediatorListening(_logger, localEndpoint.Address, localEndpoint.Port);
|
||||||
|
|
||||||
|
return base.StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await base.StopAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
_udpClient?.Dispose();
|
||||||
|
_udpClient = null;
|
||||||
|
LocalEndpoint = null;
|
||||||
|
LogMediatorStopped(_logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_udpClient?.Dispose();
|
||||||
|
_udpClient = null;
|
||||||
|
LocalEndpoint = null;
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
UdpClient udpClient = _udpClient
|
||||||
|
?? throw new InvalidOperationException("The UDP mediator socket was not initialized.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_ = await udpClient.ReceiveAsync(stoppingToken).ConfigureAwait(false);
|
||||||
|
// Bootstrap deliberately emits no UDP response. Protocol handling lands in #11.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Expected during normal shutdown.
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Disposing the socket is the fallback that releases a blocked receive.
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LocalEndpoint = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[LoggerMessage(
|
||||||
|
EventId = 1,
|
||||||
|
Level = LogLevel.Information,
|
||||||
|
Message = "UDP mediator listening on {ListenAddress}:{ListenPort}")]
|
||||||
|
private static partial void LogMediatorListening(
|
||||||
|
ILogger logger,
|
||||||
|
IPAddress listenAddress,
|
||||||
|
int listenPort);
|
||||||
|
|
||||||
|
[LoggerMessage(
|
||||||
|
EventId = 2,
|
||||||
|
Level = LogLevel.Information,
|
||||||
|
Message = "UDP mediator stopped")]
|
||||||
|
private static partial void LogMediatorStopped(ILogger logger);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"Rendezvous": {
|
||||||
|
"Udp": {
|
||||||
|
"ListenAddress": "0.0.0.0",
|
||||||
|
"Port": 9050
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"LiteNetLib": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.1.4, )",
|
||||||
|
"resolved": "2.1.4",
|
||||||
|
"contentHash": "KWlxvMw3Urpqj9joD96LRiK+LC62pQNs/zkXRJc+rHnxgkGp+vV703xzDrxRmv+V1YhCFfIGzs5nrVWtREIlyA=="
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.contracts": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<AssemblyName>FinalFactory.Rendezvous.TestClient</AssemblyName>
|
||||||
|
<RootNamespace>FinalFactory.Rendezvous.TestClient</RootNamespace>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../FinalFactory.Rendezvous.Client/FinalFactory.Rendezvous.Client.csproj" />
|
||||||
|
<ProjectReference Include="../FinalFactory.Rendezvous.Contracts/FinalFactory.Rendezvous.Contracts.csproj" />
|
||||||
|
<PackageReference Include="LiteNetLib" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace FinalFactory.Rendezvous.TestClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bootstrap entry point for the public-SDK-only diagnostic client.
|
||||||
|
/// </summary>
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the bootstrap diagnostic.
|
||||||
|
/// </summary>
|
||||||
|
public static int Main()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Rendezvous TestClient bootstrap is ready.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net8.0": {
|
||||||
|
"LiteNetLib": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.1.4, )",
|
||||||
|
"resolved": "2.1.4",
|
||||||
|
"contentHash": "KWlxvMw3Urpqj9joD96LRiK+LC62pQNs/zkXRJc+rHnxgkGp+vV703xzDrxRmv+V1YhCFfIGzs5nrVWtREIlyA=="
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.client": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
|
||||||
|
"LiteNetLib": "[2.1.4, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.contracts": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace FinalFactory.Rendezvous.Tests.Architecture;
|
||||||
|
|
||||||
|
public sealed class DependencyBoundaryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ContractsAreTransportAndEngineIndependent()
|
||||||
|
{
|
||||||
|
string[] references = GetReferences("FinalFactory.Rendezvous.Contracts");
|
||||||
|
|
||||||
|
Assert.DoesNotContain(references, IsGodotAssembly);
|
||||||
|
Assert.DoesNotContain(references, IsServerAssembly);
|
||||||
|
Assert.DoesNotContain(references, IsLiteNetLibAssembly);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClientIsGodotAndServerIndependent()
|
||||||
|
{
|
||||||
|
string[] references = GetReferences("FinalFactory.Rendezvous.Client");
|
||||||
|
|
||||||
|
Assert.DoesNotContain(references, IsGodotAssembly);
|
||||||
|
Assert.DoesNotContain(references, IsServerAssembly);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TestClientUsesOnlyPublicRendezvousDependencies()
|
||||||
|
{
|
||||||
|
string[] references = GetReferences("FinalFactory.Rendezvous.TestClient");
|
||||||
|
|
||||||
|
Assert.DoesNotContain(references, IsGodotAssembly);
|
||||||
|
Assert.DoesNotContain(references, IsServerAssembly);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeclaredDependencyGraphMatchesTheArchitecture()
|
||||||
|
{
|
||||||
|
AssertDeclaredDependencies(
|
||||||
|
"src/FinalFactory.Rendezvous.Contracts/FinalFactory.Rendezvous.Contracts.csproj",
|
||||||
|
[],
|
||||||
|
[]);
|
||||||
|
AssertDeclaredDependencies(
|
||||||
|
"src/FinalFactory.Rendezvous.Client/FinalFactory.Rendezvous.Client.csproj",
|
||||||
|
["FinalFactory.Rendezvous.Contracts"],
|
||||||
|
["LiteNetLib"]);
|
||||||
|
AssertDeclaredDependencies(
|
||||||
|
"src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Server.csproj",
|
||||||
|
["FinalFactory.Rendezvous.Contracts"],
|
||||||
|
["LiteNetLib"]);
|
||||||
|
AssertDeclaredDependencies(
|
||||||
|
"src/FinalFactory.Rendezvous.TestClient/FinalFactory.Rendezvous.TestClient.csproj",
|
||||||
|
["FinalFactory.Rendezvous.Client", "FinalFactory.Rendezvous.Contracts"],
|
||||||
|
["LiteNetLib"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertDeclaredDependencies(
|
||||||
|
string projectPath,
|
||||||
|
string[] expectedProjects,
|
||||||
|
string[] expectedPackages)
|
||||||
|
{
|
||||||
|
XDocument project = XDocument.Load(Path.Combine(FindRepositoryRoot(), projectPath));
|
||||||
|
string[] projects = project
|
||||||
|
.Descendants("ProjectReference")
|
||||||
|
.Select(static element =>
|
||||||
|
Path.GetFileNameWithoutExtension(element.Attribute("Include")?.Value) ?? string.Empty)
|
||||||
|
.Order(StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
string[] packages = project
|
||||||
|
.Descendants("PackageReference")
|
||||||
|
.Select(static element => element.Attribute("Include")?.Value ?? string.Empty)
|
||||||
|
.Order(StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(expectedProjects.Order(StringComparer.Ordinal), projects);
|
||||||
|
Assert.Equal(expectedPackages.Order(StringComparer.Ordinal), packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepositoryRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||||
|
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "Rendezvous.slnx")))
|
||||||
|
{
|
||||||
|
return directory.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("Could not locate the Rendezvous repository root.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] GetReferences(string assemblyName) => Assembly
|
||||||
|
.Load(assemblyName)
|
||||||
|
.GetReferencedAssemblies()
|
||||||
|
.Select(static reference => reference.Name ?? string.Empty)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private static bool IsGodotAssembly(string assemblyName) =>
|
||||||
|
assemblyName.StartsWith("Godot", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static bool IsLiteNetLibAssembly(string assemblyName) =>
|
||||||
|
string.Equals(assemblyName, "LiteNetLib", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static bool IsServerAssembly(string assemblyName) =>
|
||||||
|
string.Equals(
|
||||||
|
assemblyName,
|
||||||
|
"FinalFactory.Rendezvous.Server",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<AssemblyName>FinalFactory.Rendezvous.Tests</AssemblyName>
|
||||||
|
<RootNamespace>FinalFactory.Rendezvous.Tests</RootNamespace>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="xunit" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../src/FinalFactory.Rendezvous.Client/FinalFactory.Rendezvous.Client.csproj" />
|
||||||
|
<ProjectReference Include="../../src/FinalFactory.Rendezvous.Contracts/FinalFactory.Rendezvous.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="../../src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Server.csproj" />
|
||||||
|
<ProjectReference Include="../../src/FinalFactory.Rendezvous.TestClient/FinalFactory.Rendezvous.TestClient.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Net;
|
||||||
|
using FinalFactory.Rendezvous.Server.Transport;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FinalFactory.Rendezvous.Tests.Server;
|
||||||
|
|
||||||
|
public sealed class UdpMediatorServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ServiceBindsAnEphemeralUdpPortAndStopsCleanly()
|
||||||
|
{
|
||||||
|
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||||
|
UdpMediatorOptions options = new()
|
||||||
|
{
|
||||||
|
ListenAddress = IPAddress.Loopback.ToString(),
|
||||||
|
Port = 0,
|
||||||
|
};
|
||||||
|
using UdpMediatorService service = new(
|
||||||
|
Options.Create(options),
|
||||||
|
NullLogger<UdpMediatorService>.Instance);
|
||||||
|
|
||||||
|
await service.StartAsync(timeout.Token);
|
||||||
|
|
||||||
|
IPEndPoint? boundEndpoint = service.LocalEndpoint;
|
||||||
|
Assert.NotNull(boundEndpoint);
|
||||||
|
Assert.Equal(IPAddress.Loopback, boundEndpoint.Address);
|
||||||
|
Assert.InRange(boundEndpoint.Port, 1, 65_535);
|
||||||
|
|
||||||
|
await service.StopAsync(timeout.Token);
|
||||||
|
|
||||||
|
Assert.Null(service.LocalEndpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Microsoft.NET.Test.Sdk": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[18.4.0, )",
|
||||||
|
"resolved": "18.4.0",
|
||||||
|
"contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.CodeCoverage": "18.4.0",
|
||||||
|
"Microsoft.TestPlatform.TestHost": "18.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.9.3, )",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.analyzers": "1.18.0",
|
||||||
|
"xunit.assert": "2.9.3",
|
||||||
|
"xunit.core": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.runner.visualstudio": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.1.5, )",
|
||||||
|
"resolved": "3.1.5",
|
||||||
|
"contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA=="
|
||||||
|
},
|
||||||
|
"Microsoft.CodeCoverage": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "18.4.0",
|
||||||
|
"contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow=="
|
||||||
|
},
|
||||||
|
"Microsoft.TestPlatform.ObjectModel": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "18.4.0",
|
||||||
|
"contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg=="
|
||||||
|
},
|
||||||
|
"Microsoft.TestPlatform.TestHost": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "18.4.0",
|
||||||
|
"contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.TestPlatform.ObjectModel": "18.4.0",
|
||||||
|
"Newtonsoft.Json": "13.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Newtonsoft.Json": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "13.0.3",
|
||||||
|
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||||
|
},
|
||||||
|
"xunit.abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.0.3",
|
||||||
|
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
||||||
|
},
|
||||||
|
"xunit.analyzers": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.18.0",
|
||||||
|
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
|
||||||
|
},
|
||||||
|
"xunit.assert": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||||
|
},
|
||||||
|
"xunit.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.extensibility.core": "[2.9.3]",
|
||||||
|
"xunit.extensibility.execution": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.extensibility.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.abstractions": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.extensibility.execution": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.9.3",
|
||||||
|
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.extensibility.core": "[2.9.3]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.client": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
|
||||||
|
"LiteNetLib": "[2.1.4, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.contracts": {
|
||||||
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.server": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
|
||||||
|
"LiteNetLib": "[2.1.4, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finalfactory.rendezvous.testclient": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"FinalFactory.Rendezvous.Client": "[1.0.0, )",
|
||||||
|
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
|
||||||
|
"LiteNetLib": "[2.1.4, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LiteNetLib": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.1.4, )",
|
||||||
|
"resolved": "2.1.4",
|
||||||
|
"contentHash": "KWlxvMw3Urpqj9joD96LRiK+LC62pQNs/zkXRJc+rHnxgkGp+vV703xzDrxRmv+V1YhCFfIGzs5nrVWtREIlyA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user