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,162 @@
using ZB.MOM.WW.CBDDC.Core;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Network.Leadership;
namespace ZB.MOM.WW.CBDDC.Network.Tests;
public class BullyLeaderElectionServiceTests
{
private static IDiscoveryService CreateDiscovery(IList<PeerNode> peers)
{
var discovery = Substitute.For<IDiscoveryService>();
discovery.GetActivePeers().Returns(_ => peers);
return discovery;
}
private static IPeerNodeConfigurationProvider CreateConfig(string nodeId)
{
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration { NodeId = nodeId });
return configProvider;
}
/// <summary>
/// Verifies that a single node elects itself as leader.
/// </summary>
[Fact]
public async Task SingleNode_ShouldBecomeLeader()
{
var peers = new List<PeerNode>();
var electionService = new BullyLeaderElectionService(
CreateDiscovery(peers),
CreateConfig("node-A"),
electionInterval: TimeSpan.FromMilliseconds(100));
LeadershipChangedEventArgs? lastEvent = null;
electionService.LeadershipChanged += (_, e) => lastEvent = e;
await electionService.Start();
await Task.Delay(200);
electionService.IsCloudGateway.ShouldBeTrue();
electionService.CurrentGatewayNodeId.ShouldBe("node-A");
lastEvent.ShouldNotBeNull();
lastEvent!.IsLocalNodeGateway.ShouldBeTrue();
lastEvent.CurrentGatewayNodeId.ShouldBe("node-A");
await electionService.Stop();
}
/// <summary>
/// Verifies that the smallest node ID is elected as leader among LAN peers.
/// </summary>
[Fact]
public async Task MultipleNodes_SmallestNodeIdShouldBeLeader()
{
var peers = new List<PeerNode>
{
new("node-B", "192.168.1.2:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered),
new("node-C", "192.168.1.3:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered)
};
var electionService = new BullyLeaderElectionService(
CreateDiscovery(peers),
CreateConfig("node-A"),
electionInterval: TimeSpan.FromMilliseconds(100));
await electionService.Start();
await Task.Delay(200);
electionService.IsCloudGateway.ShouldBeTrue();
electionService.CurrentGatewayNodeId.ShouldBe("node-A");
await electionService.Stop();
}
/// <summary>
/// Verifies that the local node is not elected when it is not the smallest node ID.
/// </summary>
[Fact]
public async Task LocalNodeNotSmallest_ShouldNotBeLeader()
{
var peers = new List<PeerNode>
{
new("node-A", "192.168.1.1:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered),
new("node-B", "192.168.1.2:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered)
};
var electionService = new BullyLeaderElectionService(
CreateDiscovery(peers),
CreateConfig("node-C"),
electionInterval: TimeSpan.FromMilliseconds(100));
await electionService.Start();
await Task.Delay(200);
electionService.IsCloudGateway.ShouldBeFalse();
electionService.CurrentGatewayNodeId.ShouldBe("node-A");
await electionService.Stop();
}
/// <summary>
/// Verifies that leadership is re-elected when the current leader fails.
/// </summary>
[Fact]
public async Task LeaderFailure_ShouldReelect()
{
var peers = new List<PeerNode>
{
new("node-A", "192.168.1.1:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered)
};
var electionService = new BullyLeaderElectionService(
CreateDiscovery(peers),
CreateConfig("node-B"),
electionInterval: TimeSpan.FromMilliseconds(100));
var leadershipChanges = new List<LeadershipChangedEventArgs>();
electionService.LeadershipChanged += (_, e) => leadershipChanges.Add(e);
await electionService.Start();
await Task.Delay(200);
electionService.CurrentGatewayNodeId.ShouldBe("node-A");
peers.Clear();
await Task.Delay(200);
electionService.IsCloudGateway.ShouldBeTrue();
electionService.CurrentGatewayNodeId.ShouldBe("node-B");
leadershipChanges.ShouldNotBeEmpty();
leadershipChanges.Last().IsLocalNodeGateway.ShouldBeTrue();
leadershipChanges.Last().CurrentGatewayNodeId.ShouldBe("node-B");
await electionService.Stop();
}
/// <summary>
/// Verifies that cloud peers are excluded from LAN gateway election.
/// </summary>
[Fact]
public async Task CloudPeersExcludedFromElection()
{
var peers = new List<PeerNode>
{
new("node-A", "192.168.1.1:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered),
new("cloud-node-Z", "cloud.example.com:9000", DateTimeOffset.UtcNow, PeerType.CloudRemote)
};
var electionService = new BullyLeaderElectionService(
CreateDiscovery(peers),
CreateConfig("node-B"),
electionInterval: TimeSpan.FromMilliseconds(100));
await electionService.Start();
await Task.Delay(200);
electionService.CurrentGatewayNodeId.ShouldBe("node-A");
await electionService.Stop();
}
}