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:
135
tests/Hmeg/Recordingtest.Hmeg.Catalog.Tests/EngineBridgeTests.cs
Normal file
135
tests/Hmeg/Recordingtest.Hmeg.Catalog.Tests/EngineBridgeTests.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Recordingtest.Hmeg.Catalog;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Hmeg.Catalog.Tests;
|
||||
|
||||
public sealed class EngineBridgeTests
|
||||
{
|
||||
private static readonly Lazy<string?> SutRootLazy = new(FindSutRoot);
|
||||
private static string? SutRoot => SutRootLazy.Value;
|
||||
private static bool SutAvailable => SutRoot != null;
|
||||
|
||||
private static readonly string[] TargetAssemblies =
|
||||
{
|
||||
"HmEG.dll",
|
||||
"HmGeometry.dll",
|
||||
"HmGeometry.V2.dll",
|
||||
"HmTriangle.dll",
|
||||
"EditorCore.dll",
|
||||
"Editor02.HmEGAppManager.dll",
|
||||
"Editor03.PluginInterface.dll",
|
||||
};
|
||||
|
||||
private static string? FindSutRoot()
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
for (int i = 0; i < 10 && dir != null; i++)
|
||||
{
|
||||
var candidate = Path.Combine(dir.FullName, "EG-BIM Modeler");
|
||||
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "HmEG.dll")))
|
||||
return candidate;
|
||||
dir = dir.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetadataLoader_LoadsHmegAssembly_WithoutExecution()
|
||||
{
|
||||
if (!SutAvailable) { Assert.True(true, "SUT not available — skipped"); return; }
|
||||
|
||||
// MetadataLoadContext is pure metadata: it never runs type initializers
|
||||
// or any user code from the loaded assembly. The fact that we can load
|
||||
// HmEG.dll (which would otherwise need SharpDX/WPF at runtime) and
|
||||
// enumerate DefinedTypes without a TypeInitializationException is the
|
||||
// observable evidence of that guarantee.
|
||||
using var loader = new MetadataLoader(SutRoot!);
|
||||
var types = loader.LoadTypes(new[] { "HmEG.dll" }).ToList();
|
||||
Assert.True(types.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CandidateFinder_FindsSelectionRelatedTypes()
|
||||
{
|
||||
if (!SutAvailable) { Assert.True(true, "SUT not available — skipped"); return; }
|
||||
|
||||
using var loader = new MetadataLoader(SutRoot!);
|
||||
var types = loader.LoadTypes(TargetAssemblies).ToList();
|
||||
var candidates = CandidateFinder.Find(types);
|
||||
Assert.Contains(candidates, c => c.Category == "select");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CatalogSerializer_OutputsSorted_Idempotent()
|
||||
{
|
||||
if (!SutAvailable) { Assert.True(true, "SUT not available — skipped"); return; }
|
||||
|
||||
using var loader = new MetadataLoader(SutRoot!);
|
||||
var types = loader.LoadTypes(new[] { "HmEG.dll" }).ToList();
|
||||
var candidates = CandidateFinder.Find(types);
|
||||
|
||||
var tmp1 = Path.Combine(Path.GetTempPath(), "eb-cand-1-" + Guid.NewGuid().ToString("N") + ".json");
|
||||
var tmp2 = Path.Combine(Path.GetTempPath(), "eb-cand-2-" + Guid.NewGuid().ToString("N") + ".json");
|
||||
try
|
||||
{
|
||||
CatalogWriter.WriteCandidates(tmp1, candidates);
|
||||
CatalogWriter.WriteCandidates(tmp2, candidates);
|
||||
Assert.Equal(File.ReadAllBytes(tmp1), File.ReadAllBytes(tmp2));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tmp1);
|
||||
File.Delete(tmp2);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HmEgSnapshot_DefaultInstance_ThrowsNotImplemented()
|
||||
{
|
||||
var s = new HmEgSnapshot();
|
||||
Assert.Throws<NotImplementedException>(() => _ = s.SelectedObjectIds);
|
||||
Assert.Throws<NotImplementedException>(() => _ = s.Camera);
|
||||
Assert.Throws<NotImplementedException>(() => _ = s.Scene);
|
||||
Assert.Throws<NotImplementedException>(() => _ = s.IsRenderComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CandidateCategories_AllFourPresent()
|
||||
{
|
||||
if (!SutAvailable) { Assert.True(true, "SUT not available — skipped"); return; }
|
||||
|
||||
using var loader = new MetadataLoader(SutRoot!);
|
||||
var types = loader.LoadTypes(TargetAssemblies).ToList();
|
||||
var candidates = CandidateFinder.Find(types);
|
||||
|
||||
foreach (var cat in new[] { "select", "camera", "scene", "render" })
|
||||
{
|
||||
Assert.True(candidates.Any(c => c.Category == cat), $"category '{cat}' missing");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HmEgSnapshot_Constants_MatchCatalog()
|
||||
{
|
||||
if (!SutAvailable) { Assert.True(true, "SUT not available — skipped"); return; }
|
||||
|
||||
using var loader = new MetadataLoader(SutRoot!);
|
||||
var types = loader.LoadTypes(TargetAssemblies).ToList();
|
||||
var candidates = CandidateFinder.Find(types);
|
||||
|
||||
// HmEgAssemblyHint ("HmEG") should be a prefix of at least one loaded assembly.
|
||||
var asmNames = types.Select(t => t.Assembly.GetName().Name ?? "").Distinct().ToList();
|
||||
Assert.Contains(asmNames, a => a.StartsWith(HmEgSnapshot.HmEgAssemblyHint, StringComparison.Ordinal));
|
||||
|
||||
// EditorManagerTypeHint ("HmEGAppManager") should appear in at least one candidate TypeName OR type entry.
|
||||
var appearsInCandidates = candidates.Any(c =>
|
||||
c.TypeName.Contains(HmEgSnapshot.EditorManagerTypeHint, StringComparison.Ordinal));
|
||||
var appearsInTypes = types.Any(t =>
|
||||
(t.FullName ?? t.Name).Contains(HmEgSnapshot.EditorManagerTypeHint, StringComparison.Ordinal) ||
|
||||
(t.Assembly.GetName().Name ?? "").Contains(HmEgSnapshot.EditorManagerTypeHint, StringComparison.Ordinal));
|
||||
Assert.True(appearsInCandidates || appearsInTypes,
|
||||
$"EditorManagerTypeHint '{HmEgSnapshot.EditorManagerTypeHint}' not found in catalog");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.Hmeg.Catalog.Tests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Hmeg.Catalog.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\Hmeg\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user