314 lines
10 KiB
C#
Executable File
314 lines
10 KiB
C#
Executable File
using ZB.MOM.WW.CBDDC.Core;
|
|
using System.Linq;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
|
|
|
public class VectorClockTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies an empty vector clock returns the default timestamp for unknown nodes.
|
|
/// </summary>
|
|
[Fact]
|
|
public void EmptyVectorClock_ShouldReturnDefaultTimestamp()
|
|
{
|
|
// Arrange
|
|
var vc = new VectorClock();
|
|
|
|
// Act
|
|
var ts = vc.GetTimestamp("node1");
|
|
|
|
// Assert
|
|
ts.ShouldBe(default(HlcTimestamp));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies setting a timestamp stores it for the specified node.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies node identifiers are returned for all known nodes.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies equal vector clocks are compared as equal.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies a clock strictly ahead of another is reported as strictly ahead.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies a clock strictly behind another is reported as strictly behind.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies divergent per-node progress is reported as concurrent.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies pull candidates include nodes where the other clock is ahead.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies push candidates include nodes where this clock is ahead.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies a newly introduced remote node is included in pull candidates.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies merge keeps the maximum timestamp per node.
|
|
/// </summary>
|
|
[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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies cloning creates an independent copy of the vector clock.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the string representation includes serialized node timestamps.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies split-brain updates are detected as concurrent.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
}
|