BREAKING: 3-tier split step 2 + engine-bridge v3 EgBim lambdas wired
Completes the Generic / HmEG-aware / App-specific separation started in
f6b6e74. The legacy EgPlugin / EngineBridge / EngineBridge.Client /
EngineBridge.Probe modules are moved into their proper tiers, namespaces
and csproj/sln entries are renamed, and the HmegDirectStateProvider
lambdas are finally populated with real handles from the EgBim plugin
host. A new Recordingtest.Architecture.Tests project enforces the tier
rule at build time.
Moves (git mv + csproj/RootNamespace/AssemblyName rename + sln):
src/Recordingtest.EgPlugin
-> src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost
src/Recordingtest.EngineBridge
-> src/Hmeg/Recordingtest.Hmeg.Catalog
src/Recordingtest.EngineBridge.Client
-> src/Hmeg/Recordingtest.Hmeg.Bridge.Client
src/Recordingtest.EngineBridge.Probe
-> src/Hmeg/Recordingtest.Hmeg.Catalog.Probe
tests/Recordingtest.EgPlugin.Tests
-> tests/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost.Tests
tests/Recordingtest.EngineBridge.Tests
-> tests/Hmeg/Recordingtest.Hmeg.Catalog.Tests
tests/Recordingtest.EngineBridge.IntegrationTests
-> tests/Hmeg/Recordingtest.Hmeg.Catalog.IntegrationTests
Namespace rename applied across all .cs files and csproj RootNamespace:
Recordingtest.EgPlugin -> Recordingtest.Sut.EgBim.PluginHost
Recordingtest.EngineBridge -> Recordingtest.Hmeg.Catalog
Recordingtest.EngineBridge.Client -> Recordingtest.Hmeg.Bridge.Client
Recordingtest.EngineBridge.Probe -> Recordingtest.Hmeg.Catalog.Probe
New: tests/Recordingtest.Architecture.Tests/
DependencyGraphTests walks Assembly.GetReferencedAssemblies() for each
tier and fails if a forbidden reference leaks in:
- Generic modules must not reference HmEG or any app-specific DLL
- HmEG-aware modules must not reference app-specific DLLs
- Recordingtest.Hmeg.Bridge must reference HmEG (positive check)
11 tests, all passing. Prevents future drift from CLAUDE.md §8.1.
Engine-bridge v3 wire-up (HmEgBridgePlugin.BuildProvider):
Previously the HmegDirectStateProvider lambdas returned null and the
chain fell through to reflection. They now call directly into the
EditorPlugin base class that HmEgBridgePlugin inherits:
spaceProvider = () => RootSpace
// AppManager.ViewportManager.RootSpace
viewportProvider = () => View
// EGViewport : Control, HmEGViewport
documentPathProvider = () => AppManager?.FileManager?.CurrentFile
Every lambda is wrapped in try/catch so plugin construction still
cannot throw back into the SUT. Editor02.HmEGAppManager.dll added as
a reference on Recordingtest.Sut.EgBim.PluginHost.csproj — app-
specific tier, which is allowed by the architecture tests.
Entry points were confirmed from read-only review of the SUT sources at
D:\GiteaAll\EG-BIM_Modeler\EditorPluginInterface\EditorPlugin.cs
D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\HmEGAppManager.cs
D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\SubManager\FileManager.cs
closing out Q1/Q2/Q6/Q7 from docs/hmeg-api-survey.md.
Tests: 115 -> 126 (+11 Architecture), 0 failures.
Next step: live verification of /scene /camera /selection with a real
SUT session; any discrepancy in HmegDirectStateProvider reflection will
be tightened after observing real HmEG camera field names.
Ref: #10 follow-up, #14 follow-up, docs/contracts/generic-sut-split.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Recordingtest.Hmeg.Catalog.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 { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Recordingtest.Hmeg.Bridge.Client;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Hmeg.Catalog.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.Hmeg.Catalog.IntegrationTests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Hmeg.Catalog.IntegrationTests</AssemblyName>
|
||||
</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\Hmeg\Recordingtest.Hmeg.Bridge.Client\Recordingtest.Hmeg.Bridge.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user