galaxy: add ChildrenByParent index for level-at-a-time browse
This commit is contained in:
@@ -7,18 +7,21 @@ public sealed class GalaxyHierarchyIndex
|
||||
private GalaxyHierarchyIndex(
|
||||
IReadOnlyList<GalaxyObjectView> objectViews,
|
||||
IReadOnlyDictionary<int, GalaxyObjectView> objectViewsById,
|
||||
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress)
|
||||
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> childrenByParent)
|
||||
{
|
||||
ObjectViews = objectViews;
|
||||
ObjectViewsById = objectViewsById;
|
||||
TagsByAddress = tagsByAddress;
|
||||
ChildrenByParent = childrenByParent;
|
||||
}
|
||||
|
||||
/// <summary>Gets an empty Galaxy hierarchy index.</summary>
|
||||
public static GalaxyHierarchyIndex Empty { get; } = new(
|
||||
Array.Empty<GalaxyObjectView>(),
|
||||
new Dictionary<int, GalaxyObjectView>(),
|
||||
new Dictionary<string, GalaxyTagLookup>(StringComparer.OrdinalIgnoreCase));
|
||||
new Dictionary<string, GalaxyTagLookup>(StringComparer.OrdinalIgnoreCase),
|
||||
new Dictionary<int, IReadOnlyList<GalaxyObjectView>>());
|
||||
|
||||
/// <summary>Gets the object views.</summary>
|
||||
public IReadOnlyList<GalaxyObjectView> ObjectViews { get; }
|
||||
@@ -29,6 +32,9 @@ public sealed class GalaxyHierarchyIndex
|
||||
/// <summary>Gets tags indexed by address.</summary>
|
||||
public IReadOnlyDictionary<string, GalaxyTagLookup> TagsByAddress { get; }
|
||||
|
||||
/// <summary>Gets direct children grouped by parent gobject id. Root objects (no parent, or self-parented) live under key 0. Each list is sorted areas-first, then by display name (OrdinalIgnoreCase).</summary>
|
||||
public IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> ChildrenByParent { get; }
|
||||
|
||||
/// <summary>Builds a Galaxy hierarchy index from the given objects.</summary>
|
||||
/// <param name="objects">The Galaxy objects to index.</param>
|
||||
/// <returns>A new Galaxy hierarchy index.</returns>
|
||||
@@ -71,10 +77,39 @@ public sealed class GalaxyHierarchyIndex
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<int, List<GalaxyObjectView>> childrenByParent = new();
|
||||
foreach (GalaxyObjectView view in views)
|
||||
{
|
||||
int parentKey = view.Object.ParentGobjectId;
|
||||
// Treat self-parented (corrupt) rows as roots; matches DashboardBrowseTreeBuilder.
|
||||
if (parentKey == view.Object.GobjectId)
|
||||
{
|
||||
parentKey = 0;
|
||||
}
|
||||
if (!childrenByParent.TryGetValue(parentKey, out List<GalaxyObjectView>? bucket))
|
||||
{
|
||||
bucket = [];
|
||||
childrenByParent[parentKey] = bucket;
|
||||
}
|
||||
bucket.Add(view);
|
||||
}
|
||||
|
||||
foreach (List<GalaxyObjectView> bucket in childrenByParent.Values)
|
||||
{
|
||||
bucket.Sort(CompareByAreaThenDisplayName);
|
||||
}
|
||||
|
||||
Dictionary<int, IReadOnlyList<GalaxyObjectView>> readOnlyChildren = new(childrenByParent.Count);
|
||||
foreach (KeyValuePair<int, List<GalaxyObjectView>> kvp in childrenByParent)
|
||||
{
|
||||
readOnlyChildren[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
return new GalaxyHierarchyIndex(
|
||||
views,
|
||||
viewsById,
|
||||
tagsByAddress);
|
||||
tagsByAddress,
|
||||
readOnlyChildren);
|
||||
}
|
||||
|
||||
private static string BuildContainedPath(
|
||||
@@ -110,4 +145,27 @@ public sealed class GalaxyHierarchyIndex
|
||||
|
||||
return obj.TagName;
|
||||
}
|
||||
|
||||
private static int CompareByAreaThenDisplayName(GalaxyObjectView left, GalaxyObjectView right)
|
||||
{
|
||||
if (left.Object.IsArea != right.Object.IsArea)
|
||||
{
|
||||
return left.Object.IsArea ? -1 : 1;
|
||||
}
|
||||
return string.Compare(DisplayNameOf(left), DisplayNameOf(right), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string DisplayNameOf(GalaxyObjectView view)
|
||||
{
|
||||
GalaxyObject obj = view.Object;
|
||||
if (!string.IsNullOrWhiteSpace(obj.BrowseName))
|
||||
{
|
||||
return obj.BrowseName;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(obj.ContainedName))
|
||||
{
|
||||
return obj.ContainedName;
|
||||
}
|
||||
return obj.TagName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Galaxy;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for <see cref="GalaxyHierarchyIndex.ChildrenByParent"/> — the parent→children
|
||||
/// index used by the lazy browse projector (Task 3). Verifies root grouping, nested
|
||||
/// parent→child linkage, corrupt self-parented row handling, and the areas-first
|
||||
/// ordering rule shared with <c>DashboardBrowseTreeBuilder</c>.
|
||||
/// </summary>
|
||||
public sealed class GalaxyHierarchyIndexTests
|
||||
{
|
||||
/// <summary>Verifies roots (ParentGobjectId == 0) bucket under sentinel key 0.</summary>
|
||||
[Fact]
|
||||
public void ChildrenByParent_RootsUnderSentinelZero()
|
||||
{
|
||||
GalaxyObject root1 = new() { GobjectId = 1, ParentGobjectId = 0, ContainedName = "r1" };
|
||||
GalaxyObject root2 = new() { GobjectId = 2, ParentGobjectId = 0, ContainedName = "r2" };
|
||||
GalaxyObject root3 = new() { GobjectId = 3, ParentGobjectId = 0, ContainedName = "r3" };
|
||||
|
||||
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([root1, root2, root3]);
|
||||
|
||||
Assert.True(index.ChildrenByParent.TryGetValue(0, out IReadOnlyList<GalaxyObjectView>? roots));
|
||||
Assert.NotNull(roots);
|
||||
Assert.Equal(3, roots!.Count);
|
||||
Assert.Contains(roots, view => view.Object.GobjectId == 1);
|
||||
Assert.Contains(roots, view => view.Object.GobjectId == 2);
|
||||
Assert.Contains(roots, view => view.Object.GobjectId == 3);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a nested A→B→C chain links each parent to its single child bucket.</summary>
|
||||
[Fact]
|
||||
public void ChildrenByParent_NestedHierarchy_LinksParentToChildren()
|
||||
{
|
||||
GalaxyObject areaA = new() { GobjectId = 1, ParentGobjectId = 0, IsArea = true, ContainedName = "A" };
|
||||
GalaxyObject objB = new() { GobjectId = 2, ParentGobjectId = 1, ContainedName = "B" };
|
||||
GalaxyObject objC = new() { GobjectId = 3, ParentGobjectId = 2, ContainedName = "C" };
|
||||
|
||||
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([areaA, objB, objC]);
|
||||
|
||||
Assert.True(index.ChildrenByParent.TryGetValue(0, out IReadOnlyList<GalaxyObjectView>? underRoot));
|
||||
Assert.NotNull(underRoot);
|
||||
Assert.Single(underRoot!);
|
||||
Assert.Equal(1, underRoot![0].Object.GobjectId);
|
||||
|
||||
Assert.True(index.ChildrenByParent.TryGetValue(1, out IReadOnlyList<GalaxyObjectView>? underA));
|
||||
Assert.NotNull(underA);
|
||||
Assert.Single(underA!);
|
||||
Assert.Equal(2, underA![0].Object.GobjectId);
|
||||
|
||||
Assert.True(index.ChildrenByParent.TryGetValue(2, out IReadOnlyList<GalaxyObjectView>? underB));
|
||||
Assert.NotNull(underB);
|
||||
Assert.Single(underB!);
|
||||
Assert.Equal(3, underB![0].Object.GobjectId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a self-parented (corrupt) row appears under root, not under itself.</summary>
|
||||
[Fact]
|
||||
public void ChildrenByParent_SelfParentedObject_AppearsAsRoot()
|
||||
{
|
||||
GalaxyObject selfParented = new() { GobjectId = 5, ParentGobjectId = 5, ContainedName = "loop" };
|
||||
|
||||
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([selfParented]);
|
||||
|
||||
Assert.True(index.ChildrenByParent.TryGetValue(0, out IReadOnlyList<GalaxyObjectView>? roots));
|
||||
Assert.NotNull(roots);
|
||||
Assert.Single(roots!);
|
||||
Assert.Equal(5, roots![0].Object.GobjectId);
|
||||
|
||||
// The self-parented row must not appear as its own child — bucket either absent or empty.
|
||||
if (index.ChildrenByParent.TryGetValue(5, out IReadOnlyList<GalaxyObjectView>? underSelf))
|
||||
{
|
||||
Assert.Empty(underSelf!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies children sort areas-first, then by display name (case-insensitive).</summary>
|
||||
[Fact]
|
||||
public void ChildrenByParent_SortsAreasFirstThenByDisplayName()
|
||||
{
|
||||
GalaxyObject parent = new() { GobjectId = 1, ParentGobjectId = 0, IsArea = true, ContainedName = "Root" };
|
||||
GalaxyObject zebraObj = new() { GobjectId = 10, ParentGobjectId = 1, IsArea = false, ContainedName = "zebra" };
|
||||
GalaxyObject alphaArea = new() { GobjectId = 11, ParentGobjectId = 1, IsArea = true, ContainedName = "alpha" };
|
||||
GalaxyObject betaArea = new() { GobjectId = 12, ParentGobjectId = 1, IsArea = true, ContainedName = "beta" };
|
||||
|
||||
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([parent, zebraObj, alphaArea, betaArea]);
|
||||
|
||||
Assert.True(index.ChildrenByParent.TryGetValue(1, out IReadOnlyList<GalaxyObjectView>? children));
|
||||
Assert.NotNull(children);
|
||||
Assert.Equal(3, children!.Count);
|
||||
Assert.Equal(11, children[0].Object.GobjectId);
|
||||
Assert.Equal(12, children[1].Object.GobjectId);
|
||||
Assert.Equal(10, children[2].Object.GobjectId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user