From 39ba011eb452d4bc73d264814592f663580b9713 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 11:19:21 -0400 Subject: [PATCH] test(dashboard): cover summary tie-breaks, Take(10) cap, null guard --- .../DashboardGalaxySummaryProjectorTests.cs | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs index 7ce303c..2a3879e 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs @@ -145,4 +145,147 @@ public sealed class DashboardGalaxySummaryProjectorTests Assert.Equal(expected, summary.Status); } + + /// + /// Verifies that templates with equal instance counts are ordered + /// alphabetically (the ThenBy(name, OrdinalIgnoreCase) tie-break). + /// + [Fact] + public void Project_TopTemplates_TiesBreakAlphabetically() + { + // Two templates, one instance each, intentionally added B before A so the + // result order can only come from the alphabetical tie-break, not insertion. + GalaxyObject bObj = MakeObject(1, categoryId: 10, template: "$BTemplate"); + GalaxyObject aObj = MakeObject(2, categoryId: 10, template: "$ATemplate"); + GalaxyObject[] objects = [bObj, aObj]; + + GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects); + + DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); + + Assert.Equal(2, summary.TopTemplates.Count); + Assert.Equal("$ATemplate", summary.TopTemplates[0].TemplateName); + Assert.Equal(1, summary.TopTemplates[0].InstanceCount); + Assert.Equal("$BTemplate", summary.TopTemplates[1].TemplateName); + Assert.Equal(1, summary.TopTemplates[1].InstanceCount); + } + + /// + /// Verifies that categories with equal object counts are ordered by ascending + /// category id (the ThenBy(category.Key) tie-break). + /// + [Fact] + public void Project_ObjectCategories_TiesBreakByAscendingCategoryId() + { + // One object in category 26 and one in category 10 (equal counts), with the + // higher id added first so the result order comes only from the id tie-break. + GalaxyObject high = MakeObject(1, categoryId: 26, template: "$T1"); + GalaxyObject low = MakeObject(2, categoryId: 10, template: "$T2"); + GalaxyObject[] objects = [high, low]; + + GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects); + + DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); + + Assert.Equal(2, summary.ObjectCategories.Count); + Assert.Equal(10, summary.ObjectCategories[0].CategoryId); + Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName); + Assert.Equal(1, summary.ObjectCategories[0].ObjectCount); + Assert.Equal(26, summary.ObjectCategories[1].CategoryId); + Assert.Equal("OPCClient", summary.ObjectCategories[1].CategoryName); + Assert.Equal(1, summary.ObjectCategories[1].ObjectCount); + } + + /// + /// Verifies that is capped at + /// the 10 highest-usage templates, and that the dropped template is the correct + /// loser by count-then-alphabetical ordering. + /// + [Fact] + public void Project_TopTemplates_CapsAtTenHighestByCount() + { + // 11 distinct templates. "$Template00" gets the most instances; the rest each + // get a unique higher-than-baseline count so the ordering is unambiguous, + // except the two lowest ("$KeepZ" and "$DropA") tie at one instance each. + // With the count tie, "$DropA" sorts before "$KeepZ" alphabetically, so the + // Take(10) cap must KEEP "$DropA" and DROP "$KeepZ" (the alphabetical loser + // among the two tied-lowest) — proving cap order is count-then-name, not + // insertion. + List objects = new(); + int nextId = 1; + + // Nine templates with descending, distinct counts 11..3. + for (int i = 0; i < 9; i++) + { + int count = 11 - i; // 11, 10, ... 3 + string template = $"$Template{i:00}"; + for (int instance = 0; instance < count; instance++) + { + objects.Add(MakeObject(nextId++, categoryId: 10, template: template)); + } + } + + // Two templates tied at the lowest count (1 each): "$DropA" sorts before + // "$KeepZ", so the keeper at slot 10 is "$DropA" and "$KeepZ" is dropped. + objects.Add(MakeObject(nextId++, categoryId: 10, template: "$DropA")); + objects.Add(MakeObject(nextId++, categoryId: 10, template: "$KeepZ")); + + GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects.ToArray()); + + DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry); + + Assert.Equal(10, summary.TopTemplates.Count); + + // The nine distinct-count templates lead, in count order. + Assert.Equal("$Template00", summary.TopTemplates[0].TemplateName); + Assert.Equal(11, summary.TopTemplates[0].InstanceCount); + Assert.Equal("$Template08", summary.TopTemplates[8].TemplateName); + Assert.Equal(3, summary.TopTemplates[8].InstanceCount); + + // Slot 10 is "$DropA" (the alphabetical winner of the lowest-count tie); + // "$KeepZ" is the dropped loser. + Assert.Equal("$DropA", summary.TopTemplates[9].TemplateName); + Assert.Equal(1, summary.TopTemplates[9].InstanceCount); + Assert.DoesNotContain(summary.TopTemplates, usage => usage.TemplateName == "$KeepZ"); + } + + /// + /// Verifies that rejects a + /// null entry with . + /// + [Fact] + public void Project_NullEntry_Throws() + { + Assert.Throws(() => DashboardGalaxySummaryProjector.Project(null!)); + } + + private static GalaxyObject MakeObject(int gobjectId, int categoryId, string template) + { + GalaxyObject obj = new() + { + GobjectId = gobjectId, + BrowseName = $"Object{gobjectId}", + CategoryId = categoryId, + }; + obj.TemplateChain.Add(template); + return obj; + } + + private static GalaxyHierarchyCacheEntry MakeHealthyEntry(IReadOnlyList objects) + { + return new GalaxyHierarchyCacheEntry( + Status: GalaxyCacheStatus.Healthy, + Sequence: 1, + LastQueriedAt: null, + LastSuccessAt: null, + LastDeployTime: null, + LastError: null, + Objects: objects, + Index: GalaxyHierarchyIndex.Build(objects), + ObjectCount: objects.Count, + AreaCount: 0, + AttributeCount: 0, + HistorizedAttributeCount: 0, + AlarmAttributeCount: 0); + } }