69b83379d5
Fix 1 (Important): RollUp_FourLevelDeepBadQuality_ReachesRoot — proves bad quality at a 4-segment-deep leaf propagates HasBadQuality up every ancestor to the root. Fix 2 (Important): Filter_DeepLeafMatch_RetainsAllAncestorBranches — proves filtering on a terminal segment of a 3-level path retains all ancestor branches. Fix 3 (Minor): BuildAttributeTree now returns roots.AsReadOnly() so the returned IReadOnlyList<DebugTreeNode> reference is not a mutable list. Fix 4 (Minor): Added <remarks> XML doc to BuildAttributeTree noting the caller-contract that at most one AttributeValueChanged per AttributeName should be passed. All 18 DebugTreeBuilder tests pass.
236 lines
7.8 KiB
C#
236 lines
7.8 KiB
C#
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
|
|
|
/// <summary>
|
|
/// Pure unit tests for <see cref="DebugTreeBuilder.BuildAttributeTree"/> — node
|
|
/// model derivation, branch dedupe, child sort, bad-quality roll-up, and the
|
|
/// name-contains filter. No bUnit / DI required; the builder is a pure function.
|
|
/// </summary>
|
|
public class DebugTreeBuilderTests
|
|
{
|
|
private static AttributeValueChanged Attr(string name, object? value = null, string quality = "Good")
|
|
=> new("Inst", name, name, value, quality, DateTimeOffset.UtcNow);
|
|
|
|
[Fact]
|
|
public void RootLevelAttribute_BecomesSingleLeaf()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(new[] { Attr("Speed") }, null);
|
|
|
|
var node = Assert.Single(tree);
|
|
Assert.Equal("Speed", node.Key);
|
|
Assert.Equal("Speed", node.Segment);
|
|
Assert.False(node.HasChildren);
|
|
Assert.NotNull(node.Attribute);
|
|
Assert.Equal("Speed", node.Attribute!.AttributeName);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoSiblingLeaves_ShareSingleBranch_SortedBySegment()
|
|
{
|
|
// Provided out of order to prove children are sorted ordinally.
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Temp"), Attr("Motor1.Speed") }, null);
|
|
|
|
var branch = Assert.Single(tree);
|
|
Assert.Equal("Motor1", branch.Key);
|
|
Assert.Equal("Motor1", branch.Segment);
|
|
Assert.True(branch.HasChildren);
|
|
Assert.Null(branch.Attribute);
|
|
|
|
Assert.Equal(2, branch.Children.Count);
|
|
Assert.Equal("Motor1.Speed", branch.Children[0].Key);
|
|
Assert.Equal("Speed", branch.Children[0].Segment);
|
|
Assert.Equal("Motor1.Temp", branch.Children[1].Key);
|
|
Assert.Equal("Temp", branch.Children[1].Segment);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeepPath_NestsBranchesByAccumulatedPrefix()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Compressor.Pump") }, null);
|
|
|
|
var motor = Assert.Single(tree);
|
|
Assert.Equal("Motor1", motor.Key);
|
|
Assert.True(motor.HasChildren);
|
|
|
|
var compressor = Assert.Single(motor.Children);
|
|
Assert.Equal("Motor1.Compressor", compressor.Key);
|
|
Assert.Equal("Compressor", compressor.Segment);
|
|
Assert.True(compressor.HasChildren);
|
|
Assert.Null(compressor.Attribute);
|
|
|
|
var pump = Assert.Single(compressor.Children);
|
|
Assert.Equal("Motor1.Compressor.Pump", pump.Key);
|
|
Assert.Equal("Pump", pump.Segment);
|
|
Assert.False(pump.HasChildren);
|
|
Assert.NotNull(pump.Attribute);
|
|
}
|
|
|
|
[Fact]
|
|
public void BranchDedupe_SharedPrefixAcrossDeepPaths()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[]
|
|
{
|
|
Attr("Motor1.Compressor.Pump"),
|
|
Attr("Motor1.Compressor.Valve"),
|
|
Attr("Motor1.Speed"),
|
|
},
|
|
null);
|
|
|
|
var motor = Assert.Single(tree);
|
|
Assert.Equal("Motor1", motor.Key);
|
|
// Two direct children: "Compressor" (branch) and "Speed" (leaf), sorted.
|
|
Assert.Equal(2, motor.Children.Count);
|
|
Assert.Equal("Motor1.Compressor", motor.Children[0].Key);
|
|
Assert.Equal("Motor1.Speed", motor.Children[1].Key);
|
|
|
|
var compressor = motor.Children[0];
|
|
Assert.Equal(2, compressor.Children.Count);
|
|
Assert.Equal("Motor1.Compressor.Pump", compressor.Children[0].Key);
|
|
Assert.Equal("Motor1.Compressor.Valve", compressor.Children[1].Key);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollUp_BadQualityDescendant_MarksAllAncestorBranches()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[]
|
|
{
|
|
Attr("Motor1.Compressor.Pump", quality: "Bad"),
|
|
Attr("Motor1.Speed", quality: "Good"),
|
|
},
|
|
null);
|
|
|
|
var motor = Assert.Single(tree);
|
|
Assert.True(motor.HasBadQuality);
|
|
|
|
var compressor = motor.Children.Single(c => c.Key == "Motor1.Compressor");
|
|
Assert.True(compressor.HasBadQuality);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollUp_AllGood_LeavesBranchHasBadQualityFalse()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Speed"), Attr("Motor1.Temp") }, null);
|
|
|
|
var motor = Assert.Single(tree);
|
|
Assert.False(motor.HasBadQuality);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Uncertain")]
|
|
[InlineData("Bad")]
|
|
public void RollUp_NonGoodQuality_CountsAsBad(string quality)
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Speed", quality: quality) }, null);
|
|
|
|
Assert.True(Assert.Single(tree).HasBadQuality);
|
|
}
|
|
|
|
[Fact]
|
|
public void Filter_KeepsOnlyMatchingLeaves_AndTheirAncestors()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[]
|
|
{
|
|
Attr("Motor1.Temp"),
|
|
Attr("Motor1.Speed"),
|
|
Attr("Motor2.Pressure"),
|
|
},
|
|
"temp");
|
|
|
|
// Only Motor1.Temp matches → just the Motor1 branch with one Temp leaf.
|
|
var motor = Assert.Single(tree);
|
|
Assert.Equal("Motor1", motor.Key);
|
|
var leaf = Assert.Single(motor.Children);
|
|
Assert.Equal("Motor1.Temp", leaf.Key);
|
|
}
|
|
|
|
[Fact]
|
|
public void Filter_IsCaseInsensitive()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Temp"), Attr("Motor1.Speed") }, "TEMP");
|
|
|
|
var motor = Assert.Single(tree);
|
|
Assert.Equal("Motor1.Temp", Assert.Single(motor.Children).Key);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void Filter_EmptyOrWhitespace_ReturnsFullForest(string? filter)
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Temp"), Attr("Motor2.Pressure") }, filter);
|
|
|
|
Assert.Equal(2, tree.Count);
|
|
Assert.Equal("Motor1", tree[0].Key);
|
|
Assert.Equal("Motor2", tree[1].Key);
|
|
}
|
|
|
|
[Fact]
|
|
public void Filter_NoMatches_ReturnsEmptyForest()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Temp") }, "nonexistent");
|
|
|
|
Assert.Empty(tree);
|
|
}
|
|
|
|
[Fact]
|
|
public void TopLevelForest_SortedBySegment()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Zeta"), Attr("Alpha"), Attr("Motor1.Speed") }, null);
|
|
|
|
Assert.Equal(3, tree.Count);
|
|
Assert.Equal("Alpha", tree[0].Key);
|
|
Assert.Equal("Motor1", tree[1].Key);
|
|
Assert.Equal("Zeta", tree[2].Key);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyInput_ReturnsEmptyForest()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(Array.Empty<AttributeValueChanged>(), null);
|
|
Assert.Empty(tree);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollUp_FourLevelDeepBadQuality_ReachesRoot()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("A.B.C.D", quality: "Bad") }, null);
|
|
|
|
var a = Assert.Single(tree);
|
|
Assert.True(a.HasBadQuality);
|
|
var b = Assert.Single(a.Children);
|
|
Assert.True(b.HasBadQuality);
|
|
var c = Assert.Single(b.Children);
|
|
Assert.True(c.HasBadQuality);
|
|
var d = Assert.Single(c.Children);
|
|
Assert.True(d.HasBadQuality);
|
|
}
|
|
|
|
[Fact]
|
|
public void Filter_DeepLeafMatch_RetainsAllAncestorBranches()
|
|
{
|
|
var tree = DebugTreeBuilder.BuildAttributeTree(
|
|
new[] { Attr("Motor1.Compressor.Pump"), Attr("Motor1.Speed") }, "Pump");
|
|
|
|
var motor = Assert.Single(tree);
|
|
Assert.Equal("Motor1", motor.Key);
|
|
var compressor = Assert.Single(motor.Children);
|
|
Assert.Equal("Motor1.Compressor", compressor.Key);
|
|
Assert.Equal("Motor1.Compressor.Pump", Assert.Single(compressor.Children).Key);
|
|
}
|
|
}
|