208 lines
5.6 KiB
C#
208 lines
5.6 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;
|
|
|
|
/// <summary>
|
|
/// The v3 identity authority (<see cref="RawPaths"/>): segment validation, slash-joined build,
|
|
/// combine/split/leaf/parent, and the case-sensitive ordinal comparer. Every identity seam in v3
|
|
/// keys on the strings this helper produces, so the separator + segment charset contract is pinned
|
|
/// here in exactly one place.
|
|
/// </summary>
|
|
public sealed class RawPathsTests
|
|
{
|
|
// ---- constants ----
|
|
|
|
[Fact]
|
|
public void Separator_is_forward_slash()
|
|
{
|
|
RawPaths.Separator.ShouldBe('/');
|
|
RawPaths.SeparatorString.ShouldBe("/");
|
|
}
|
|
|
|
[Fact]
|
|
public void Comparer_is_ordinal_case_sensitive()
|
|
{
|
|
RawPaths.Comparer.ShouldBe(StringComparer.Ordinal);
|
|
RawPaths.Comparer.Equals("Line/Tag", "line/tag").ShouldBeFalse();
|
|
RawPaths.Comparer.Equals("Line/Tag", "Line/Tag").ShouldBeTrue();
|
|
}
|
|
|
|
// ---- ValidateSegment / IsValidSegment ----
|
|
|
|
[Theory]
|
|
[InlineData("Modbus")]
|
|
[InlineData("Device_001")]
|
|
[InlineData("Tag.Name")] // dots are legal inside a segment
|
|
[InlineData("a")]
|
|
[InlineData("Has Space")] // interior whitespace is legal; only leading/trailing is not
|
|
public void ValidateSegment_accepts_legal_segments(string segment)
|
|
{
|
|
RawPaths.ValidateSegment(segment).ShouldBeNull();
|
|
RawPaths.IsValidSegment(segment).ShouldBeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
public void ValidateSegment_rejects_empty(string? segment)
|
|
{
|
|
RawPaths.ValidateSegment(segment).ShouldNotBeNull();
|
|
RawPaths.IsValidSegment(segment).ShouldBeFalse();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("a/b")]
|
|
[InlineData("/leading")]
|
|
[InlineData("trailing/")]
|
|
public void ValidateSegment_rejects_separator(string segment)
|
|
{
|
|
RawPaths.ValidateSegment(segment).ShouldNotBeNull();
|
|
RawPaths.ValidateSegment(segment)!.ShouldContain("/");
|
|
RawPaths.IsValidSegment(segment).ShouldBeFalse();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(" leading")]
|
|
[InlineData("trailing ")]
|
|
[InlineData("\ttab")]
|
|
[InlineData("nl\n")]
|
|
public void ValidateSegment_rejects_edge_whitespace(string segment)
|
|
{
|
|
RawPaths.ValidateSegment(segment).ShouldNotBeNull();
|
|
RawPaths.IsValidSegment(segment).ShouldBeFalse();
|
|
}
|
|
|
|
// ---- Build ----
|
|
|
|
[Fact]
|
|
public void Build_joins_multiple_segments_with_separator()
|
|
{
|
|
RawPaths.Build("Folder", "Modbus", "Dev", "Group", "Tag")
|
|
.ShouldBe("Folder/Modbus/Dev/Group/Tag");
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_single_segment_is_the_segment()
|
|
{
|
|
RawPaths.Build("Solo").ShouldBe("Solo");
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_enumerable_overload_joins()
|
|
{
|
|
RawPaths.Build(new List<string> { "A", "B" }).ShouldBe("A/B");
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_empty_list_throws()
|
|
{
|
|
Should.Throw<ArgumentException>(() => RawPaths.Build(Array.Empty<string>()));
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_null_throws()
|
|
{
|
|
Should.Throw<ArgumentNullException>(() => RawPaths.Build((IEnumerable<string>)null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_invalid_segment_throws_naming_the_index()
|
|
{
|
|
var ex = Should.Throw<ArgumentException>(() => RawPaths.Build("ok", "bad/seg", "also-ok"));
|
|
ex.Message.ShouldContain("index 1");
|
|
}
|
|
|
|
// ---- Combine ----
|
|
|
|
[Fact]
|
|
public void Combine_blank_parent_yields_child_alone()
|
|
{
|
|
RawPaths.Combine(null, "Root").ShouldBe("Root");
|
|
RawPaths.Combine("", "Root").ShouldBe("Root");
|
|
}
|
|
|
|
[Fact]
|
|
public void Combine_appends_child_under_parent()
|
|
{
|
|
RawPaths.Combine("A/B", "C").ShouldBe("A/B/C");
|
|
}
|
|
|
|
[Fact]
|
|
public void Combine_invalid_child_throws()
|
|
{
|
|
Should.Throw<ArgumentException>(() => RawPaths.Combine("A/B", "c/d"));
|
|
Should.Throw<ArgumentException>(() => RawPaths.Combine("A/B", ""));
|
|
}
|
|
|
|
// ---- Split ----
|
|
|
|
[Fact]
|
|
public void Split_returns_ordered_segments()
|
|
{
|
|
RawPaths.Split("A/B/C").ShouldBe(new[] { "A", "B", "C" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Split_single_segment_is_one_element()
|
|
{
|
|
RawPaths.Split("Solo").ShouldBe(new[] { "Solo" });
|
|
}
|
|
|
|
// ---- Leaf ----
|
|
|
|
[Fact]
|
|
public void Leaf_of_multi_segment_is_last()
|
|
{
|
|
RawPaths.Leaf("A/B/C").ShouldBe("C");
|
|
}
|
|
|
|
[Fact]
|
|
public void Leaf_of_single_segment_is_itself()
|
|
{
|
|
RawPaths.Leaf("Solo").ShouldBe("Solo");
|
|
}
|
|
|
|
// ---- TryParent ----
|
|
|
|
[Fact]
|
|
public void TryParent_of_multi_segment_returns_parent()
|
|
{
|
|
RawPaths.TryParent("A/B/C", out var parent).ShouldBeTrue();
|
|
parent.ShouldBe("A/B");
|
|
}
|
|
|
|
[Fact]
|
|
public void TryParent_of_two_segment_returns_root()
|
|
{
|
|
RawPaths.TryParent("A/B", out var parent).ShouldBeTrue();
|
|
parent.ShouldBe("A");
|
|
}
|
|
|
|
[Fact]
|
|
public void TryParent_of_single_segment_returns_false()
|
|
{
|
|
RawPaths.TryParent("Solo", out var parent).ShouldBeFalse();
|
|
parent.ShouldBe(string.Empty);
|
|
}
|
|
|
|
// ---- round trips ----
|
|
|
|
[Fact]
|
|
public void Build_then_Split_round_trips()
|
|
{
|
|
var segs = new[] { "Folder", "Sub", "Modbus", "Dev", "Tag" };
|
|
RawPaths.Split(RawPaths.Build(segs)).ShouldBe(segs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Combine_then_Leaf_and_Parent_are_consistent()
|
|
{
|
|
var path = RawPaths.Combine("A/B", "C");
|
|
RawPaths.Leaf(path).ShouldBe("C");
|
|
RawPaths.TryParent(path, out var parent).ShouldBeTrue();
|
|
parent.ShouldBe("A/B");
|
|
}
|
|
}
|