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,93 @@
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Sut.EgBim.PluginHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
|
||||
|
||||
public class ChainedEngineStateProviderTests
|
||||
{
|
||||
private sealed class ScriptedProvider : IEngineStateProvider
|
||||
{
|
||||
public IReadOnlyList<string> SelectedIds = Array.Empty<string>();
|
||||
public CameraSnapshot Camera = new(
|
||||
new double[] { 0, 0, 0 },
|
||||
new double[] { 0, 0, 0 },
|
||||
new double[] { 0, 0, 1 },
|
||||
45.0);
|
||||
public SceneSnapshot Scene = new(0, null);
|
||||
public bool Render = true;
|
||||
|
||||
public IReadOnlyList<string> GetSelectedIds() => SelectedIds;
|
||||
public CameraSnapshot GetCamera() => Camera;
|
||||
public SceneSnapshot GetScene() => Scene;
|
||||
public bool GetRenderComplete() => Render;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_Primary_NonEmpty_Wins()
|
||||
{
|
||||
var p = new ScriptedProvider { SelectedIds = new[] { "a" } };
|
||||
var f = new ScriptedProvider { SelectedIds = new[] { "fallback" } };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(new[] { "a" }, c.GetSelectedIds());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_Primary_Empty_Falls_Through()
|
||||
{
|
||||
var p = new ScriptedProvider();
|
||||
var f = new ScriptedProvider { SelectedIds = new[] { "fallback" } };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(new[] { "fallback" }, c.GetSelectedIds());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Camera_Primary_Default_Falls_Through()
|
||||
{
|
||||
var p = new ScriptedProvider();
|
||||
var fc = new CameraSnapshot(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 }, new double[] { 0, 0, 1 }, 60);
|
||||
var f = new ScriptedProvider { Camera = fc };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(60.0, c.GetCamera().Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Camera_Primary_NonDefault_Wins()
|
||||
{
|
||||
var pc = new CameraSnapshot(new double[] { 1, 0, 0 }, new double[] { 0, 0, 0 }, new double[] { 0, 0, 1 }, 30);
|
||||
var p = new ScriptedProvider { Camera = pc };
|
||||
var f = new ScriptedProvider();
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(30.0, c.GetCamera().Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scene_Primary_Empty_Falls_Through()
|
||||
{
|
||||
var p = new ScriptedProvider();
|
||||
var f = new ScriptedProvider { Scene = new(7, "x.hmeg") };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
var s = c.GetScene();
|
||||
Assert.Equal(7, s.ObjectCount);
|
||||
Assert.Equal("x.hmeg", s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scene_Primary_Has_Path_Wins_Even_With_Zero_Objects()
|
||||
{
|
||||
var p = new ScriptedProvider { Scene = new(0, "primary.hmeg") };
|
||||
var f = new ScriptedProvider { Scene = new(99, "fallback.hmeg") };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
var s = c.GetScene();
|
||||
Assert.Equal("primary.hmeg", s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Primary_Always_Wins()
|
||||
{
|
||||
var p = new ScriptedProvider { Render = false };
|
||||
var f = new ScriptedProvider { Render = true };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.False(c.GetRenderComplete());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<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.Sut.EgBim.PluginHost.Tests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Sut.EgBim.PluginHost.Tests</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\Sut\EgBim\Recordingtest.Sut.EgBim.PluginHost\Recordingtest.Sut.EgBim.PluginHost.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,149 @@
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Sut.EgBim.PluginHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
|
||||
|
||||
public class ReflectionEngineStateProviderTests
|
||||
{
|
||||
private sealed class FakeAccessor : IAppManagerAccessor
|
||||
{
|
||||
public object? AppManager { get; set; } = new object();
|
||||
public object? ActiveDocument { get; set; }
|
||||
public object? ActiveViewport { get; set; }
|
||||
public IReadOnlyList<string> SelectedIds { get; set; } = Array.Empty<string>();
|
||||
public int ObjectCount { get; set; }
|
||||
public string? DocumentPath { get; set; }
|
||||
public (double[] Eye, double[] Target, double[] Up, double Fov)? Camera { get; set; }
|
||||
public bool ThrowOnSelection { get; set; }
|
||||
public bool ThrowOnCamera { get; set; }
|
||||
public bool ThrowOnScene { get; set; }
|
||||
|
||||
public object? GetAppManager() => AppManager;
|
||||
public object? GetActiveDocument() => ActiveDocument;
|
||||
public object? GetActiveViewport() => ActiveViewport;
|
||||
public IReadOnlyList<string> GetSelectedIds()
|
||||
{
|
||||
if (ThrowOnSelection) throw new InvalidOperationException("boom");
|
||||
return SelectedIds;
|
||||
}
|
||||
public int GetObjectCount()
|
||||
{
|
||||
if (ThrowOnScene) throw new InvalidOperationException("boom");
|
||||
return ObjectCount;
|
||||
}
|
||||
public string? GetDocumentPath()
|
||||
{
|
||||
if (ThrowOnScene) throw new InvalidOperationException("boom");
|
||||
return DocumentPath;
|
||||
}
|
||||
public (double[] Eye, double[] Target, double[] Up, double Fov)? GetCameraTuple()
|
||||
{
|
||||
if (ThrowOnCamera) throw new InvalidOperationException("boom");
|
||||
return Camera;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSelectedIds_Returns_Accessor_Values()
|
||||
{
|
||||
var fa = new FakeAccessor { SelectedIds = new[] { "a", "b" } };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var ids = p.GetSelectedIds();
|
||||
Assert.Equal(new[] { "a", "b" }, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSelectedIds_Swallows_Exception_Returns_Empty()
|
||||
{
|
||||
var fa = new FakeAccessor { ThrowOnSelection = true };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
Assert.Empty(p.GetSelectedIds());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCamera_Returns_Accessor_Tuple()
|
||||
{
|
||||
var fa = new FakeAccessor
|
||||
{
|
||||
Camera = (
|
||||
Eye: new double[] { 1, 2, 3 },
|
||||
Target: new double[] { 4, 5, 6 },
|
||||
Up: new double[] { 0, 0, 1 },
|
||||
Fov: 60.0)
|
||||
};
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(new double[] { 1, 2, 3 }, c.Eye);
|
||||
Assert.Equal(new double[] { 4, 5, 6 }, c.Target);
|
||||
Assert.Equal(60.0, c.Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCamera_Null_Tuple_Returns_Default()
|
||||
{
|
||||
var fa = new FakeAccessor { Camera = null };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
Assert.Equal(new double[] { 0, 0, 1 }, c.Up);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCamera_Throwing_Accessor_Returns_Default_Not_Throw()
|
||||
{
|
||||
var fa = new FakeAccessor { ThrowOnCamera = true };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetScene_Returns_Accessor_Values()
|
||||
{
|
||||
var fa = new FakeAccessor { ObjectCount = 42, DocumentPath = "C:/x.hmeg" };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(42, s.ObjectCount);
|
||||
Assert.Equal("C:/x.hmeg", s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetScene_Throwing_Accessor_Returns_Empty_Default()
|
||||
{
|
||||
var fa = new FakeAccessor { ThrowOnScene = true };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(0, s.ObjectCount);
|
||||
Assert.Null(s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_Reflection_Accessor_Without_Hmeg_Returns_Null_Safely()
|
||||
{
|
||||
// CI process has no Editor.AppManager.AppManager type loaded; the
|
||||
// accessor must return null/empty/default without throwing.
|
||||
var acc = new ReflectionAppManagerAccessor(seedRoot: new object());
|
||||
Assert.Null(acc.GetAppManager());
|
||||
Assert.Null(acc.GetActiveDocument());
|
||||
Assert.Null(acc.GetActiveViewport());
|
||||
Assert.Empty(acc.GetSelectedIds());
|
||||
Assert.Equal(0, acc.GetObjectCount());
|
||||
Assert.Null(acc.GetDocumentPath());
|
||||
Assert.Null(acc.GetCameraTuple());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Provider_With_Default_Accessor_Without_Hmeg_Returns_Defaults()
|
||||
{
|
||||
// End-to-end fallback: provider built from a non-HmEG seed must
|
||||
// produce the v2 safe defaults so /state continues to respond.
|
||||
var p = new ReflectionEngineStateProvider(seedRoot: new object());
|
||||
Assert.Empty(p.GetSelectedIds());
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(0, s.ObjectCount);
|
||||
Assert.True(p.GetRenderComplete());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Net;
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Sut.EgBim.PluginHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user