fca978de07
Sweep of 203 source files resolving CommentChecker findings: add <summary>/<param>/<returns>/<inheritdoc> where missing, and remove resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN, Task N) from code comments. Comment/doc-only — no logic changes. Server+Tests build clean under TreatWarningsAsErrors.
294 lines
12 KiB
C#
294 lines
12 KiB
C#
using ZB.MOM.WW.GalaxyRepository;
|
|
using ZB.MOM.WW.GalaxyRepository.Grpc;
|
|
using ZB.MOM.WW.MxGateway.Server.Dashboard;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="DashboardGalaxySummaryProjector"/>, the host-side
|
|
/// projection that turns a shared-library <see cref="GalaxyHierarchyCacheEntry"/>
|
|
/// into the dashboard's <see cref="DashboardGalaxySummary"/>.
|
|
/// </summary>
|
|
public sealed class DashboardGalaxySummaryProjectorTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies that a healthy entry maps status, copies timestamps/counts, and
|
|
/// groups objects into template usage (ordered by instance count) and
|
|
/// category counts with resolved names.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Project_HealthyEntry_MapsStatusCopiesCountsAndGroupsObjects()
|
|
{
|
|
DateTimeOffset queriedAt = new(2026, 6, 25, 10, 0, 0, TimeSpan.Zero);
|
|
DateTimeOffset successAt = new(2026, 6, 25, 10, 0, 1, TimeSpan.Zero);
|
|
DateTimeOffset deployAt = new(2026, 6, 24, 8, 30, 0, TimeSpan.Zero);
|
|
|
|
// Two templates (PumpTemplate x2, ValveTemplate x1) and two categories
|
|
// (10 = UserDefined x3, 13 = Area x1).
|
|
GalaxyObject area = new()
|
|
{
|
|
GobjectId = 1,
|
|
BrowseName = "AreaA",
|
|
IsArea = true,
|
|
CategoryId = 13,
|
|
};
|
|
GalaxyObject pumpA = new()
|
|
{
|
|
GobjectId = 2,
|
|
BrowseName = "Pump01",
|
|
CategoryId = 10,
|
|
};
|
|
pumpA.TemplateChain.Add("$PumpTemplate");
|
|
GalaxyObject pumpB = new()
|
|
{
|
|
GobjectId = 3,
|
|
BrowseName = "Pump02",
|
|
CategoryId = 10,
|
|
};
|
|
pumpB.TemplateChain.Add("$PumpTemplate");
|
|
GalaxyObject valve = new()
|
|
{
|
|
GobjectId = 4,
|
|
BrowseName = "Valve01",
|
|
CategoryId = 10,
|
|
};
|
|
valve.TemplateChain.Add("$ValveTemplate");
|
|
|
|
GalaxyObject[] objects = [area, pumpA, pumpB, valve];
|
|
|
|
GalaxyHierarchyCacheEntry entry = new(
|
|
Status: GalaxyCacheStatus.Healthy,
|
|
Sequence: 7,
|
|
LastQueriedAt: queriedAt,
|
|
LastSuccessAt: successAt,
|
|
LastDeployTime: deployAt,
|
|
LastError: null,
|
|
Objects: objects,
|
|
Index: GalaxyHierarchyIndex.Build(objects),
|
|
ObjectCount: 4,
|
|
AreaCount: 1,
|
|
AttributeCount: 12,
|
|
HistorizedAttributeCount: 5,
|
|
AlarmAttributeCount: 2);
|
|
|
|
DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
|
|
|
|
Assert.Equal(DashboardGalaxyStatus.Healthy, summary.Status);
|
|
Assert.Equal(queriedAt, summary.LastQueriedAt);
|
|
Assert.Equal(successAt, summary.LastSuccessAt);
|
|
Assert.Equal(deployAt, summary.LastDeployTime);
|
|
Assert.Null(summary.LastError);
|
|
Assert.Equal(4, summary.ObjectCount);
|
|
Assert.Equal(1, summary.AreaCount);
|
|
Assert.Equal(12, summary.AttributeCount);
|
|
Assert.Equal(5, summary.HistorizedAttributeCount);
|
|
Assert.Equal(2, summary.AlarmAttributeCount);
|
|
|
|
// Templates ordered by instance count descending: PumpTemplate (2) before ValveTemplate (1).
|
|
Assert.Equal(2, summary.TopTemplates.Count);
|
|
Assert.Equal("$PumpTemplate", summary.TopTemplates[0].TemplateName);
|
|
Assert.Equal(2, summary.TopTemplates[0].InstanceCount);
|
|
Assert.Equal("$ValveTemplate", summary.TopTemplates[1].TemplateName);
|
|
Assert.Equal(1, summary.TopTemplates[1].InstanceCount);
|
|
|
|
// Categories ordered by object count descending: UserDefined (3) before Area (1).
|
|
Assert.Equal(2, summary.ObjectCategories.Count);
|
|
Assert.Equal(10, summary.ObjectCategories[0].CategoryId);
|
|
Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName);
|
|
Assert.Equal(3, summary.ObjectCategories[0].ObjectCount);
|
|
Assert.Equal(13, summary.ObjectCategories[1].CategoryId);
|
|
Assert.Equal("Area", summary.ObjectCategories[1].CategoryName);
|
|
Assert.Equal(1, summary.ObjectCategories[1].ObjectCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that an unknown entry with no objects projects to the
|
|
/// <see cref="DashboardGalaxySummary.Unknown"/>-equivalent summary.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Project_UnknownEmptyEntry_ProducesUnknownEquivalentSummary()
|
|
{
|
|
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty;
|
|
|
|
DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
|
|
|
|
Assert.Equal(DashboardGalaxyStatus.Unknown, summary.Status);
|
|
Assert.Null(summary.LastQueriedAt);
|
|
Assert.Null(summary.LastSuccessAt);
|
|
Assert.Null(summary.LastDeployTime);
|
|
Assert.Null(summary.LastError);
|
|
Assert.Equal(0, summary.ObjectCount);
|
|
Assert.Equal(0, summary.AreaCount);
|
|
Assert.Equal(0, summary.AttributeCount);
|
|
Assert.Equal(0, summary.HistorizedAttributeCount);
|
|
Assert.Equal(0, summary.AlarmAttributeCount);
|
|
Assert.Empty(summary.TopTemplates);
|
|
Assert.Empty(summary.ObjectCategories);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the cache status enum is faithfully mapped to the dashboard
|
|
/// status enum across every defined value.
|
|
/// </summary>
|
|
/// <param name="cacheStatus">The Galaxy cache status to project.</param>
|
|
/// <param name="expected">The expected dashboard status for the given cache status.</param>
|
|
[Theory]
|
|
[InlineData(GalaxyCacheStatus.Healthy, DashboardGalaxyStatus.Healthy)]
|
|
[InlineData(GalaxyCacheStatus.Stale, DashboardGalaxyStatus.Stale)]
|
|
[InlineData(GalaxyCacheStatus.Unavailable, DashboardGalaxyStatus.Unavailable)]
|
|
[InlineData(GalaxyCacheStatus.Unknown, DashboardGalaxyStatus.Unknown)]
|
|
public void Project_MapsCacheStatusToDashboardStatus(
|
|
GalaxyCacheStatus cacheStatus,
|
|
DashboardGalaxyStatus expected)
|
|
{
|
|
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with { Status = cacheStatus };
|
|
|
|
DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
|
|
|
|
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);
|
|
}
|
|
}
|