- Add Recordingtest.EngineBridge library (IEngineSnapshot, HmEgSnapshot skeleton, MetadataLoader, CandidateFinder, CatalogWriter). - Add Recordingtest.EngineBridge.Probe console exe that dumps hmeg-types.json and hmeg-candidates.json to docs/engine-catalog. - Add Recordingtest.EngineBridge.Tests (xUnit, 6 tests). - Add probe design doc with plugin-masquerade recommendation. - Static analysis only; SUT is never executed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
5.3 KiB
C#
136 lines
5.3 KiB
C#
using System.Reflection;
|
|
using System.Text.Json;
|
|
using Recordingtest.EngineBridge;
|
|
using Xunit;
|
|
|
|
namespace Recordingtest.EngineBridge.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");
|
|
}
|
|
}
|