test(dashboard): cover summary tie-breaks, Take(10) cap, null guard

This commit is contained in:
Joseph Doherty
2026-06-25 11:19:21 -04:00
parent 555e56fdfb
commit 39ba011eb4
@@ -145,4 +145,147 @@ public sealed class DashboardGalaxySummaryProjectorTests
Assert.Equal(expected, summary.Status); Assert.Equal(expected, summary.Status);
} }
/// <summary>
/// Verifies that templates with equal instance counts are ordered
/// alphabetically (the <c>ThenBy(name, OrdinalIgnoreCase)</c> tie-break).
/// </summary>
[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);
}
/// <summary>
/// Verifies that categories with equal object counts are ordered by ascending
/// category id (the <c>ThenBy(category.Key)</c> tie-break).
/// </summary>
[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);
}
/// <summary>
/// Verifies that <see cref="DashboardGalaxySummary.TopTemplates"/> is capped at
/// the 10 highest-usage templates, and that the dropped template is the correct
/// loser by count-then-alphabetical ordering.
/// </summary>
[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<GalaxyObject> 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");
}
/// <summary>
/// Verifies that <see cref="DashboardGalaxySummaryProjector.Project"/> rejects a
/// null entry with <see cref="ArgumentNullException"/>.
/// </summary>
[Fact]
public void Project_NullEntry_Throws()
{
Assert.Throws<ArgumentNullException>(() => 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<GalaxyObject> 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);
}
} }