Implement engine-bridge v2 plugin masquerade (#10)

This commit is contained in:
minsung
2026-04-07 16:08:31 +09:00
parent 4cee3c2d86
commit b1c2383a54
18 changed files with 1017 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<RootNamespace>Recordingtest.EgPlugin.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Recordingtest.EgPlugin\Recordingtest.EgPlugin.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,64 @@
using System.Net;
using Recordingtest.EgPlugin;
using Xunit;
namespace Recordingtest.EgPlugin.Tests;
public class StateRouterTests
{
private sealed class FixedProvider : IEngineStateProvider
{
public IReadOnlyList<string> GetSelectedIds() => new[] { "x", "y" };
public CameraSnapshot GetCamera() => new(new double[] { 1, 2, 3 }, new double[] { 0, 0, 0 }, new double[] { 0, 0, 1 }, 45);
public SceneSnapshot GetScene() => new(7, "doc.hmeg");
public bool GetRenderComplete() => true;
}
private sealed class FaultyProvider : IEngineStateProvider
{
public IReadOnlyList<string> GetSelectedIds() => throw new InvalidOperationException("boom");
public CameraSnapshot GetCamera() => throw new InvalidOperationException();
public SceneSnapshot GetScene() => throw new InvalidOperationException();
public bool GetRenderComplete() => throw new InvalidOperationException();
}
[Fact]
public void StateRouter_SelectionPath_UsesProvider_ReturnsJson()
{
var r = new StateRouter(new FixedProvider(), 38080);
var (status, body) = r.Route("/selection");
Assert.Equal(HttpStatusCode.OK, status);
Assert.Contains("\"selected_ids\":[\"x\",\"y\"]", body);
}
[Fact]
public void StateRouter_FaultyProvider_ReturnsErrorPayload()
{
var r = new StateRouter(new FaultyProvider(), 38080);
var (_, body) = r.Route("/selection");
Assert.Contains("\"error\"", body);
Assert.Contains("boom", body);
}
[Fact]
public void StateRouter_UnknownPath_Returns404()
{
var r = new StateRouter(new FixedProvider(), 38080);
var (status, _) = r.Route("/nope");
Assert.Equal(HttpStatusCode.NotFound, status);
}
[Fact]
public void PortResolver_EnvVarSet_ReturnsEnvPort()
{
var p = PortResolver.Resolve(_ => "45000");
Assert.Equal(45000, p);
}
[Fact]
public void PortResolver_EnvVarMissing_ReturnsDefault()
{
var p = PortResolver.Resolve(_ => null);
Assert.Equal(PortResolver.DefaultPort, p);
}
}

View File

@@ -0,0 +1,72 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Recordingtest.EngineBridge.IntegrationTests;
public sealed class FakeBridgeServer : IDisposable
{
public Dictionary<string, string> Responses { get; } = new();
public TimeSpan ResponseDelay { get; set; } = TimeSpan.Zero;
private readonly HttpListener _listener;
private readonly Thread _thread;
private volatile bool _stop;
public int Port { get; }
public string BaseUrl => $"http://localhost:{Port}";
public FakeBridgeServer()
{
Port = FindFreePort();
_listener = new HttpListener();
_listener.Prefixes.Add($"http://localhost:{Port}/");
_listener.Start();
_thread = new Thread(Loop) { IsBackground = true };
_thread.Start();
}
private static int FindFreePort()
{
var l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
var p = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return p;
}
private void Loop()
{
while (!_stop && _listener.IsListening)
{
HttpListenerContext ctx;
try { ctx = _listener.GetContext(); }
catch { return; }
try
{
if (ResponseDelay > TimeSpan.Zero) Thread.Sleep(ResponseDelay);
var path = ctx.Request.Url?.AbsolutePath ?? "/";
if (Responses.TryGetValue(path, out var body))
{
var bytes = Encoding.UTF8.GetBytes(body);
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "application/json";
ctx.Response.OutputStream.Write(bytes, 0, bytes.Length);
}
else
{
ctx.Response.StatusCode = 404;
}
ctx.Response.OutputStream.Close();
}
catch { try { ctx.Response.Abort(); } catch { } }
}
}
public void Dispose()
{
_stop = true;
try { _listener.Stop(); } catch { }
try { _listener.Close(); } catch { }
}
}

View File

@@ -0,0 +1,66 @@
using Recordingtest.EngineBridge.Client;
using Xunit;
namespace Recordingtest.EngineBridge.IntegrationTests;
public class HmEgHttpSnapshotTests
{
[Fact]
public void Client_SelectionEndpoint_ReturnsIds()
{
using var srv = new FakeBridgeServer();
srv.Responses["/selection"] = "{\"selected_ids\":[\"a\",\"b\"]}";
using var c = new HmEgHttpSnapshot(srv.BaseUrl);
Assert.Equal(new[] { "a", "b" }, c.SelectedObjectIds);
}
[Fact]
public void Client_CameraEndpoint_ReturnsCameraState()
{
using var srv = new FakeBridgeServer();
srv.Responses["/camera"] = "{\"eye\":[1,2,3],\"target\":[4,5,6],\"up\":[0,0,1],\"fov\":50}";
using var c = new HmEgHttpSnapshot(srv.BaseUrl);
var cam = c.Camera;
Assert.Equal(new double[] { 1, 2, 3 }, cam.EyePoint);
Assert.Equal(new double[] { 4, 5, 6 }, cam.Target);
Assert.Equal(50, cam.Fov);
}
[Fact]
public void Client_SceneEndpoint_ReturnsSceneSummary()
{
using var srv = new FakeBridgeServer();
srv.Responses["/scene"] = "{\"object_count\":42,\"document_path\":\"C:/m.hmeg\"}";
using var c = new HmEgHttpSnapshot(srv.BaseUrl);
var s = c.Scene;
Assert.Equal(42, s.ObjectCount);
Assert.Equal("C:/m.hmeg", s.DocumentPath);
}
[Fact]
public void Client_RenderEndpoint_ReturnsIsComplete()
{
using var srv = new FakeBridgeServer();
srv.Responses["/render"] = "{\"complete\":true}";
using var c = new HmEgHttpSnapshot(srv.BaseUrl);
Assert.True(c.IsRenderComplete);
}
[Fact]
public void Client_HealthEndpoint_ReturnsOk()
{
using var srv = new FakeBridgeServer();
srv.Responses["/health"] = "{\"status\":\"ok\",\"port\":1}";
using var c = new HmEgHttpSnapshot(srv.BaseUrl);
Assert.True(c.IsHealthy);
}
[Fact]
public void Client_Timeout_ThrowsEngineBridgeException()
{
using var srv = new FakeBridgeServer { ResponseDelay = TimeSpan.FromSeconds(5) };
srv.Responses["/selection"] = "{\"selected_ids\":[]}";
using var c = new HmEgHttpSnapshot(srv.BaseUrl, timeout: TimeSpan.FromMilliseconds(500));
Assert.Throws<EngineBridgeException>(() => _ = c.SelectedObjectIds);
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<RootNamespace>Recordingtest.EngineBridge.IntegrationTests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Recordingtest.EngineBridge.Client\Recordingtest.EngineBridge.Client.csproj" />
</ItemGroup>
</Project>