using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; using ZB.MOM.WW.MxGateway.Server.Dashboard; using ZB.MOM.WW.MxGateway.Server.Galaxy; namespace ZB.MOM.WW.MxGateway.Tests.Galaxy; /// /// Direct coverage for paging. /// /// Regression guard for finding Server-007: the projector memoizes the filtered, /// ordered view list per (cache entry, filter signature) so paging is an /// O(pageSize) slice rather than an O(total) re-scan per page. These tests confirm /// the memo does not change paging results, does not bleed between distinct filter /// signatures, and is scoped to a single cache-entry instance. /// /// public sealed class GalaxyHierarchyProjectorTests { /// Verifies that paging across a hierarchy returns every object exactly once. [Fact] public void Project_PagedAcrossEntireHierarchy_ReturnsEveryObjectExactlyOnce() { GalaxyHierarchyCacheEntry entry = CreateEntry(CreateObjects(25)); List collected = []; int totalReported = -1; for (int offset = 0; offset < 25; offset += 4) { GalaxyHierarchyQueryResult result = GalaxyHierarchyProjector.Project( entry, new DiscoverHierarchyRequest(), browseSubtreeGlobs: null, offset, pageSize: 4); totalReported = result.TotalObjectCount; collected.AddRange(result.Objects.Select(obj => obj.TagName)); } Assert.Equal(25, totalReported); Assert.Equal(25, collected.Count); Assert.Equal(collected.Count, collected.Distinct(StringComparer.Ordinal).Count()); Assert.Equal("Object_001", collected[0]); Assert.Equal("Object_025", collected[^1]); } /// Verifies that distinct filters on the same entry do not share memoized view list. [Fact] public void Project_DistinctFiltersOnSameEntry_DoNotShareMemoizedViewList() { GalaxyHierarchyCacheEntry entry = CreateEntry(CreateObjects(10)); GalaxyHierarchyQueryResult globbed = GalaxyHierarchyProjector.Project( entry, new DiscoverHierarchyRequest { TagNameGlob = "Object_00?" }); GalaxyHierarchyQueryResult unfiltered = GalaxyHierarchyProjector.Project( entry, new DiscoverHierarchyRequest()); // Distinct filter signatures must each get their own filtered list. Assert.Equal(9, globbed.TotalObjectCount); Assert.Equal(10, unfiltered.TotalObjectCount); } /// Verifies that the same filter repeated returns identical totals. [Fact] public void Project_SameFilterRepeated_ReturnsIdenticalTotals() { GalaxyHierarchyCacheEntry entry = CreateEntry(CreateObjects(12)); GalaxyHierarchyQueryResult first = GalaxyHierarchyProjector.Project( entry, new DiscoverHierarchyRequest(), browseSubtreeGlobs: null, offset: 0, pageSize: 5); GalaxyHierarchyQueryResult second = GalaxyHierarchyProjector.Project( entry, new DiscoverHierarchyRequest(), browseSubtreeGlobs: null, offset: 5, pageSize: 5); Assert.Equal(first.TotalObjectCount, second.TotalObjectCount); Assert.Equal(first.FilterSignature, second.FilterSignature); Assert.Equal(5, first.Objects.Count); Assert.Equal(5, second.Objects.Count); Assert.NotEqual(first.Objects[0].TagName, second.Objects[0].TagName); } /// Verifies that distinct cache entries project against their own data. [Fact] public void Project_DistinctCacheEntries_ProjectAgainstTheirOwnData() { GalaxyHierarchyCacheEntry small = CreateEntry(CreateObjects(3)); GalaxyHierarchyCacheEntry large = CreateEntry(CreateObjects(40)); GalaxyHierarchyQueryResult smallResult = GalaxyHierarchyProjector.Project( small, new DiscoverHierarchyRequest()); GalaxyHierarchyQueryResult largeResult = GalaxyHierarchyProjector.Project( large, new DiscoverHierarchyRequest()); // Each entry instance keys its own memo; the second projection must not reuse the // first entry's filtered view list. Assert.Equal(3, smallResult.TotalObjectCount); Assert.Equal(40, largeResult.TotalObjectCount); } private static GalaxyHierarchyCacheEntry CreateEntry(IReadOnlyList objects) { return GalaxyHierarchyCacheEntry.Empty with { Status = GalaxyCacheStatus.Healthy, Sequence = 1, LastSuccessAt = DateTimeOffset.UtcNow, Objects = objects, Index = GalaxyHierarchyIndex.Build(objects), DashboardSummary = DashboardGalaxySummary.Unknown with { Status = DashboardGalaxyStatus.Healthy, ObjectCount = objects.Count, }, ObjectCount = objects.Count, }; } private static IReadOnlyList CreateObjects(int count) { return Enumerable.Range(1, count) .Select(index => new GalaxyObject { GobjectId = index, TagName = $"Object_{index:000}", BrowseName = $"Object_{index:000}", }) .ToArray(); } }