feat(debugview): DV-3 DebugTreeNode model + attribute tree builder
Pure path-split composition forest from streamed AttributeValueChanged: branch dedupe by accumulated prefix, ordinal child sort, post-order bad-quality roll-up, case-insensitive name-contains filter (keeps ancestors). BuildAlarmTree left as a NotImplementedException stub for DV-4. 16 unit tests cover structure + roll-up + filter.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user