Initial import of the CBDDC codebase with docs and tests. Add a .NET-focused gitignore to keep generated artifacts out of source control.
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
Joseph Doherty
2026-02-20 13:03:21 -05:00
commit 08bfc17218
218 changed files with 33910 additions and 0 deletions

View File

@@ -0,0 +1,313 @@
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");
}
}