feat(galaxy): nest gobject browse tree by parent_gobject_id (degrade-to-flat)
This commit is contained in:
+118
-3
@@ -23,7 +23,9 @@ public sealed class GalaxyDiscovererTests
|
||||
=> Task.FromResult(objects);
|
||||
}
|
||||
|
||||
private sealed record FolderCall(string BrowseName, string DisplayName);
|
||||
/// <summary><c>ParentBrowseName</c> is the browse-name of the folder this folder was created under
|
||||
/// (<c>null</c> = created at the driver root).</summary>
|
||||
private sealed record FolderCall(string BrowseName, string DisplayName, string? ParentBrowseName);
|
||||
private sealed record VariableCall(string FolderBrowseName, string AttributeName, DriverAttributeInfo Info);
|
||||
|
||||
private sealed class FakeBuilder : IAddressSpaceBuilder
|
||||
@@ -47,7 +49,8 @@ public sealed class GalaxyDiscovererTests
|
||||
/// <returns>An IAddressSpaceBuilder scoped to the new folder.</returns>
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{
|
||||
Folders.Add(new FolderCall(browseName, displayName));
|
||||
// Root-level folder — no parent.
|
||||
Folders.Add(new FolderCall(browseName, displayName, ParentBrowseName: null));
|
||||
// Return a child scoped to this folder; nested folders inherit the parent reference.
|
||||
return new ChildBuilder(this, browseName);
|
||||
}
|
||||
@@ -79,7 +82,8 @@ public sealed class GalaxyDiscovererTests
|
||||
/// <returns>An IAddressSpaceBuilder scoped to the new child folder.</returns>
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
{
|
||||
parent.Folders.Add(new FolderCall(browseName, displayName));
|
||||
// Nested folder — record the folder it was created under.
|
||||
parent.Folders.Add(new FolderCall(browseName, displayName, ParentBrowseName: folderBrowseName));
|
||||
return new ChildBuilder(parent, browseName);
|
||||
}
|
||||
|
||||
@@ -155,6 +159,22 @@ public sealed class GalaxyDiscovererTests
|
||||
return o;
|
||||
}
|
||||
|
||||
/// <summary>Builds a gobject with explicit id + parent id so nesting-by-parent can be asserted.</summary>
|
||||
private static GalaxyObject Node(
|
||||
int gobjectId, int parentGobjectId, string tagName,
|
||||
string? containedName = null, params GalaxyAttribute[] attributes)
|
||||
{
|
||||
var o = new GalaxyObject
|
||||
{
|
||||
GobjectId = gobjectId,
|
||||
ParentGobjectId = parentGobjectId,
|
||||
TagName = tagName,
|
||||
ContainedName = containedName ?? tagName,
|
||||
};
|
||||
o.Attributes.AddRange(attributes);
|
||||
return o;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that discovery creates one folder per object and one variable per attribute.</summary>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_BuildsOneFolderPerObject_AndOneVariablePerAttribute()
|
||||
@@ -333,6 +353,101 @@ public sealed class GalaxyDiscovererTests
|
||||
builder.Variables.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Child gobject's folder is created UNDER its parent's folder, not at the root.</summary>
|
||||
[Fact]
|
||||
public async Task Nests_child_folder_under_its_parent()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Node(1, 0, "Parent", "parent", Attr("PV")),
|
||||
Node(2, 1, "Child", "child", Attr("PV")),
|
||||
]);
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await new GalaxyDiscoverer(src).DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
var parent = builder.Folders.Single(f => f.BrowseName == "parent");
|
||||
parent.ParentBrowseName.ShouldBeNull();
|
||||
var child = builder.Folders.Single(f => f.BrowseName == "child");
|
||||
child.ParentBrowseName.ShouldBe("parent");
|
||||
}
|
||||
|
||||
/// <summary>Nesting is order-independent: child returned before its parent still nests correctly.</summary>
|
||||
[Fact]
|
||||
public async Task Is_order_independent_child_before_parent()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Node(2, 1, "Child", "child", Attr("PV")), // child FIRST
|
||||
Node(1, 0, "Parent", "parent", Attr("PV")),
|
||||
]);
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await new GalaxyDiscoverer(src).DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
var child = builder.Folders.Single(f => f.BrowseName == "child");
|
||||
child.ParentBrowseName.ShouldBe("parent");
|
||||
}
|
||||
|
||||
/// <summary>Both gobjects with parent=0 land flat at the driver root.</summary>
|
||||
[Fact]
|
||||
public async Task Degrades_to_flat_when_parent_is_zero()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Node(1, 0, "A", "a", Attr("PV")),
|
||||
Node(2, 0, "B", "b", Attr("PV")),
|
||||
]);
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await new GalaxyDiscoverer(src).DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
builder.Folders.Single(f => f.BrowseName == "a").ParentBrowseName.ShouldBeNull();
|
||||
builder.Folders.Single(f => f.BrowseName == "b").ParentBrowseName.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>A child whose parent_gobject_id is not in the returned set degrades to the root.</summary>
|
||||
[Fact]
|
||||
public async Task Degrades_to_flat_when_parent_not_in_set()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Node(2, 99, "Child", "child", Attr("PV")), // parent 99 absent
|
||||
]);
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await new GalaxyDiscoverer(src).DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
builder.Folders.Single(f => f.BrowseName == "child").ParentBrowseName.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>A self-referential parent (id == parent id) attaches to the root, never to itself.</summary>
|
||||
[Fact]
|
||||
public async Task Self_parent_attaches_to_root()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Node(5, 5, "Selfie", "selfie", Attr("PV")),
|
||||
]);
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await new GalaxyDiscoverer(src).DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
builder.Folders.Single(f => f.BrowseName == "selfie").ParentBrowseName.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>A child object's attributes are added into the child's own folder, not the parent's.</summary>
|
||||
[Fact]
|
||||
public async Task Variables_land_in_their_owner_folder()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Node(1, 0, "Parent", "parent", Attr("ParentPV")),
|
||||
Node(2, 1, "Child", "child", Attr("ChildPV")),
|
||||
]);
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await new GalaxyDiscoverer(src).DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
builder.Variables.ShouldContain(v => v.FolderBrowseName == "parent" && v.AttributeName == "ParentPV");
|
||||
builder.Variables.ShouldContain(v => v.FolderBrowseName == "child" && v.AttributeName == "ChildPV");
|
||||
builder.Variables.ShouldNotContain(v => v.FolderBrowseName == "parent" && v.AttributeName == "ChildPV");
|
||||
}
|
||||
|
||||
/// <summary>Helper that exercises the internal ctor (test seam) without exposing it publicly.</summary>
|
||||
private sealed class GalaxyDriverHelper
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user