diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs new file mode 100644 index 0000000..b38726b --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs @@ -0,0 +1,181 @@ +// Copyright 2012-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using Shouldly; + +namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; + +public sealed class RaftNodeTests +{ + [Fact] + public void NRGAppendEntryEncode_ShouldSucceed() + { + var raft = new Raft(); + var ae = raft.NewAppendEntry("N1", 2, 1, 1, 0, [raft.NewEntry(EntryType.EntryNormal, [1])]); + var enc = ae.Encode(); + enc.Length.ShouldBeGreaterThan(0); + } + + [Fact] + public void NRGAppendEntryDecode_ShouldSucceed() + { + var raft = new Raft(); + var ae = raft.NewAppendEntry("N1", 2, 1, 1, 0, [raft.NewEntry(EntryType.EntryNormal, [1])]); + var dec = raft.DecodeAppendEntry(ae.Encode()); + dec.Leader.ShouldBe("N1"); + dec.TermV.ShouldBe(2UL); + } + + [Fact] + public void NRGInlineStepdown_ShouldSucceed() + { + var raft = new Raft { StateValue = (int)RaftState.Leader }; + raft.StepdownLocked("N2"); + raft.State().ShouldBe(RaftState.Follower); + raft.LeaderId.ShouldBe("N2"); + } + + [Fact] + public void NRGAEFromOldLeader_ShouldSucceed() + { + var raft = new Raft { Term_ = 4 }; + var ae = raft.NewAppendEntry("L1", 3, 1, 2, 0, []); + raft.ProcessAppendEntries(ae); + raft.Term_.ShouldBe(4UL); + } + + [Fact] + public void NRGLeaderTransfer_ShouldSucceed() + { + var raft = new Raft { StateValue = (int)RaftState.Follower }; + raft.XferCampaign().ShouldBeNull(); + raft.State().ShouldBe(RaftState.Candidate); + } + + [Fact] + public void NRGHeartbeatOnLeaderChange_ShouldSucceed() + { + var raft = new Raft { StateValue = (int)RaftState.Follower }; + raft.RunAsLeader(); + raft.Leader().ShouldBeTrue(); + raft.LeadChangeC().ShouldNotBeNull(); + } + + [Fact] + public void NRGElectionTimerAfterObserver_ShouldSucceed() + { + var raft = new Raft { StateValue = (int)RaftState.Follower }; + raft.SetObserverInternal(true); + raft.ResetElectionTimeout(); + raft.Elect.ShouldNotBeNull(); + } + + [Fact] + public void NRGRemoveLeaderPeerDeadlockBug_ShouldSucceed() + { + var raft = new Raft { Id = "N1", StateValue = (int)RaftState.Leader }; + raft.ProposeRemovePeer("N2"); + raft.MembershipChangeInProgress().ShouldBeTrue(); + } + + [Fact] + public void NRGPendingAppendEntryCacheInvalidation_ShouldSucceed() + { + var raft = new Raft { GroupName = "RG" }; + var ae = raft.NewAppendEntry("N1", 1, 1, 0, 0, [raft.NewEntry(EntryType.EntryNormal, [1])]); + raft.ProcessAppendEntries(ae); + raft.LoadFirstEntry().ShouldNotBeNull(); + } + + [Fact] + public void NRGVoteResponseEncoding_ShouldSucceed() + { + var raft = new Raft(); + var vr = new VoteResponse { TermV = 2, Peer = "N1", Granted = true }; + var decoded = raft.DecodeVoteResponse(vr.Encode()); + decoded.Peer.ShouldBe("N1"); + decoded.Granted.ShouldBeTrue(); + } + + [Fact] + public void NRGProposeRemovePeer_ShouldSucceed() + { + var raft = new Raft { PIndex = 5 }; + raft.ProposeRemovePeer("N2"); + raft.Removed.ContainsKey("N2").ShouldBeTrue(); + } + + [Fact] + public void NRGProposeRemovePeerConcurrent_ShouldSucceed() + { + var raft = new Raft { PIndex = 10 }; + Parallel.For(0, 4, i => raft.ProposeRemovePeer($"N{i}")); + raft.Removed.Count.ShouldBeGreaterThan(0); + } + + [Fact] + public void NRGProposeRemovePeerQuorum_ShouldSucceed() + { + var raft = new Raft { Qn = 2, Csz = 3 }; + raft.ProposeRemovePeer("N2"); + raft.ClusterSize().ShouldBe(1); + } + + [Fact] + public void NRGProposeRemovePeerLeader_ShouldSucceed() + { + var raft = new Raft { Id = "N1", StateValue = (int)RaftState.Leader }; + raft.ProposeRemovePeer("N2"); + raft.State().ShouldBe(RaftState.Leader); + } + + [Fact] + public void NRGProposeRemovePeerAll_ShouldSucceed() + { + var raft = new Raft(); + raft.ProposeRemovePeer("N2"); + raft.ProposeRemovePeer("N3"); + raft.Removed.Count.ShouldBe(2); + } + + [Fact] + public void NRGLeaderResurrectsRemovedPeers_ShouldSucceed() + { + var raft = new Raft(); + raft.ProposeRemovePeer("N2"); + raft.ProposeAddPeer("N2"); + raft.Peers_.ContainsKey("N2").ShouldBeTrue(); + } + + [Fact] + public void NRGAddPeers_ShouldSucceed() + { + var raft = new Raft(); + raft.AddPeer("N2"); + raft.AddPeer("N3"); + raft.Peers_.Count.ShouldBe(2); + } + + [Fact] + public void NRGDisjointMajorities_ShouldSucceed() + { + var raft = new Raft + { + Qn = 3, + Peers_ = new Dictionary + { + ["N2"] = new() { Ts = DateTime.UtcNow }, + ["N3"] = new() { Ts = DateTime.UtcNow }, + }, + }; + raft.LostQuorum().ShouldBeFalse(); + } + + [Fact] + public void NRGSingleNodeElection_ShouldSucceed() + { + var raft = new Raft { Csz = 1, Qn = 1, StateValue = (int)RaftState.Follower }; + raft.CampaignInternal(TimeSpan.FromMilliseconds(10)).ShouldBeNull(); + raft.State().ShouldBe(RaftState.Candidate); + } +} diff --git a/porting.db b/porting.db index b7dfa50..e558601 100644 Binary files a/porting.db and b/porting.db differ