using ZB.MOM.WW.CBDDC.Core; using System.Linq; using Xunit; namespace ZB.MOM.WW.CBDDC.Core.Tests; public class VectorClockTests { /// /// Verifies an empty vector clock returns the default timestamp for unknown nodes. /// [Fact] public void EmptyVectorClock_ShouldReturnDefaultTimestamp() { // Arrange var vc = new VectorClock(); // Act var ts = vc.GetTimestamp("node1"); // Assert ts.ShouldBe(default(HlcTimestamp)); } /// /// Verifies setting a timestamp stores it for the specified node. /// [Fact] public void SetTimestamp_ShouldStoreTimestamp() { // Arrange var vc = new VectorClock(); var ts = new HlcTimestamp(100, 1, "node1"); // Act vc.SetTimestamp("node1", ts); // Assert vc.GetTimestamp("node1").ShouldBe(ts); } /// /// Verifies node identifiers are returned for all known nodes. /// [Fact] public void NodeIds_ShouldReturnAllNodes() { // Arrange var vc = new VectorClock(); vc.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Act var nodeIds = vc.NodeIds.ToList(); // Assert nodeIds.Count.ShouldBe(2); nodeIds.ShouldContain("node1"); nodeIds.ShouldContain("node2"); } /// /// Verifies equal vector clocks are compared as equal. /// [Fact] public void CompareTo_EqualClocks_ShouldReturnEqual() { // Arrange var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc1.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc2.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Act var result = vc1.CompareTo(vc2); // Assert result.ShouldBe(CausalityRelation.Equal); } /// /// Verifies a clock strictly ahead of another is reported as strictly ahead. /// [Fact] public void CompareTo_StrictlyAhead_ShouldReturnStrictlyAhead() { // Arrange var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); // Ahead vc1.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Same var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc2.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Act var result = vc1.CompareTo(vc2); // Assert result.ShouldBe(CausalityRelation.StrictlyAhead); } /// /// Verifies a clock strictly behind another is reported as strictly behind. /// [Fact] public void CompareTo_StrictlyBehind_ShouldReturnStrictlyBehind() { // Arrange var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); // Behind vc1.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Same var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); vc2.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Act var result = vc1.CompareTo(vc2); // Assert result.ShouldBe(CausalityRelation.StrictlyBehind); } /// /// Verifies divergent per-node progress is reported as concurrent. /// [Fact] public void CompareTo_Concurrent_ShouldReturnConcurrent() { // Arrange - Split brain scenario var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); // Node1 ahead vc1.SetTimestamp("node2", new HlcTimestamp(100, 2, "node2")); // Node2 behind var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); // Node1 behind vc2.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Node2 ahead // Act var result = vc1.CompareTo(vc2); // Assert result.ShouldBe(CausalityRelation.Concurrent); } /// /// Verifies pull candidates include nodes where the other clock is ahead. /// [Fact] public void GetNodesWithUpdates_ShouldReturnNodesWhereOtherIsAhead() { // Arrange var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc1.SetTimestamp("node2", new HlcTimestamp(100, 2, "node2")); var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); // Ahead vc2.SetTimestamp("node2", new HlcTimestamp(100, 2, "node2")); // Same // Act var nodesToPull = vc1.GetNodesWithUpdates(vc2).ToList(); // Assert nodesToPull.Count().ShouldBe(1); nodesToPull.ShouldContain("node1"); } /// /// Verifies push candidates include nodes where this clock is ahead. /// [Fact] public void GetNodesToPush_ShouldReturnNodesWhereThisIsAhead() { // Arrange var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); // Ahead vc1.SetTimestamp("node2", new HlcTimestamp(100, 2, "node2")); // Same var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc2.SetTimestamp("node2", new HlcTimestamp(100, 2, "node2")); // Act var nodesToPush = vc1.GetNodesToPush(vc2).ToList(); // Assert nodesToPush.Count().ShouldBe(1); nodesToPush.ShouldContain("node1"); } /// /// Verifies a newly introduced remote node is included in pull candidates. /// [Fact] public void GetNodesWithUpdates_WhenNewNodeAppearsInOther_ShouldReturnIt() { // Arrange - Simulates a new node joining the cluster var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc2.SetTimestamp("node3", new HlcTimestamp(50, 1, "node3")); // New node // Act var nodesToPull = vc1.GetNodesWithUpdates(vc2).ToList(); // Assert nodesToPull.Count().ShouldBe(1); nodesToPull.ShouldContain("node3"); } /// /// Verifies merge keeps the maximum timestamp per node. /// [Fact] public void Merge_ShouldTakeMaximumForEachNode() { // Arrange var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(200, 1, "node1")); vc1.SetTimestamp("node2", new HlcTimestamp(100, 2, "node2")); var vc2 = new VectorClock(); vc2.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc2.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); vc2.SetTimestamp("node3", new HlcTimestamp(150, 1, "node3")); // Act vc1.Merge(vc2); // Assert vc1.GetTimestamp("node1").ShouldBe(new HlcTimestamp(200, 1, "node1")); // Kept max vc1.GetTimestamp("node2").ShouldBe(new HlcTimestamp(200, 2, "node2")); // Merged max vc1.GetTimestamp("node3").ShouldBe(new HlcTimestamp(150, 1, "node3")); // Added new } /// /// Verifies cloning creates an independent copy of the vector clock. /// [Fact] public void Clone_ShouldCreateIndependentCopy() { // Arrange var vc1 = new VectorClock(); vc1.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); // Act var vc2 = vc1.Clone(); vc2.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Assert vc1.NodeIds.Count().ShouldBe(1); vc2.NodeIds.Count().ShouldBe(2); } /// /// Verifies the string representation includes serialized node timestamps. /// [Fact] public void ToString_ShouldReturnReadableFormat() { // Arrange var vc = new VectorClock(); vc.SetTimestamp("node1", new HlcTimestamp(100, 1, "node1")); vc.SetTimestamp("node2", new HlcTimestamp(200, 2, "node2")); // Act var str = vc.ToString(); // Assert str.ShouldContain("node1:100:1:node1"); str.ShouldContain("node2:200:2:node2"); } /// /// Verifies split-brain updates are detected as concurrent. /// [Fact] public void SplitBrainScenario_ShouldDetectConcurrency() { // Arrange - Simulating a network partition scenario // Partition 1: node1 and node2 are alive var vcPartition1 = new VectorClock(); vcPartition1.SetTimestamp("node1", new HlcTimestamp(300, 5, "node1")); vcPartition1.SetTimestamp("node2", new HlcTimestamp(250, 3, "node2")); vcPartition1.SetTimestamp("node3", new HlcTimestamp(100, 1, "node3")); // Old data // Partition 2: node3 is isolated var vcPartition2 = new VectorClock(); vcPartition2.SetTimestamp("node1", new HlcTimestamp(150, 2, "node1")); // Old data vcPartition2.SetTimestamp("node2", new HlcTimestamp(150, 1, "node2")); // Old data vcPartition2.SetTimestamp("node3", new HlcTimestamp(400, 8, "node3")); // New data // Act var relation = vcPartition1.CompareTo(vcPartition2); var partition1NeedsToPull = vcPartition1.GetNodesWithUpdates(vcPartition2).ToList(); var partition1NeedsToPush = vcPartition1.GetNodesToPush(vcPartition2).ToList(); // Assert relation.ShouldBe(CausalityRelation.Concurrent); partition1NeedsToPull.Count().ShouldBe(1); partition1NeedsToPull.ShouldContain("node3"); partition1NeedsToPush.Count.ShouldBe(2); partition1NeedsToPush.ShouldContain("node1"); partition1NeedsToPush.ShouldContain("node2"); } }