Files
2026-02-28 20:17:45 -05:00

206 lines
6.5 KiB
C#

// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using Shouldly;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class RaftNodeCoreTests
{
[Fact]
public void SnapshotHelpers_WhenEncodedAndLoaded_ShouldRoundTrip()
{
var storeDir = Path.Combine(Path.GetTempPath(), $"raft-node-core-{Guid.NewGuid():N}");
var raft = new Raft
{
StoreDir = storeDir,
Term_ = 3,
Applied_ = 9,
PApplied = 7,
Wps = [1, 2, 3],
};
try
{
var checkpoint = raft.CreateSnapshotCheckpointLocked(force: true);
checkpoint.ShouldNotBeNull();
var snapshot = new Snapshot
{
LastTerm = 3,
LastIndex = 9,
PeerState = [7, 8],
Data = [9, 10, 11],
};
raft.InstallSnapshotInternal(snapshot).ShouldBeNull();
var (loaded, error) = raft.LoadLastSnapshot();
error.ShouldBeNull();
loaded.ShouldNotBeNull();
loaded!.LastTerm.ShouldBe(3UL);
loaded.LastIndex.ShouldBe(9UL);
loaded.PeerState.ShouldBe([7, 8]);
loaded.Data.ShouldBe([9, 10, 11]);
raft.SetupLastSnapshot().ShouldBeNull();
raft.PIndex.ShouldBe(9UL);
raft.PTerm.ShouldBe(3UL);
}
finally
{
if (Directory.Exists(storeDir))
{
Directory.Delete(storeDir, recursive: true);
}
}
}
[Fact]
public void LeadershipHelpers_WhenSteppingDownAndSelectingLeader_ShouldUpdateState()
{
var raft = new Raft
{
Id = "N1",
StateValue = (int)RaftState.Candidate,
Peers_ = new Dictionary<string, Lps>
{
["N2"] = new() { Li = 3 },
["N3"] = new() { Li = 7 },
},
};
raft.StepdownLocked("N3");
raft.State().ShouldBe(RaftState.Follower);
raft.LeaderId.ShouldBe("N3");
raft.SelectNextLeader().ShouldBe("N3");
}
[Fact]
public void CampaignHelpers_WhenLeaderOrFollower_ShouldReturnExpectedOutcome()
{
var raft = new Raft
{
StateValue = (int)RaftState.Leader,
};
raft.CampaignInternal(TimeSpan.FromMilliseconds(200)).ShouldNotBeNull();
raft.XferCampaign().ShouldNotBeNull();
raft.StateValue = (int)RaftState.Follower;
raft.CampaignInternal(TimeSpan.FromMilliseconds(200)).ShouldBeNull();
raft.State().ShouldBe(RaftState.Candidate);
}
[Fact]
public void ProgressHelpers_WhenCatchupAndKnownPeersChange_ShouldTrackFlags()
{
var raft = new Raft
{
Commit = 10,
Applied_ = 8,
Catchup = new CatchupState(),
};
raft.IsCatchingUp().ShouldBeTrue();
raft.IsCurrent(includeForwardProgress: true).ShouldBeFalse();
raft.Catchup = null;
raft.UpdateKnownPeersLocked(["N2", "N3"]);
raft.Peers_.Count.ShouldBe(2);
raft.RandCampaignTimeout().ShouldBeGreaterThan(TimeSpan.Zero);
}
[Fact]
public void RunLoopHelpers_WhenInvoked_ShouldManageSubscriptionAndTimers()
{
var raft = new Raft
{
Id = "N1",
GroupName = "RG",
Active = DateTime.UtcNow,
};
var inbox = raft.NewInbox();
var catchupInbox = raft.NewCatchupInbox();
inbox.ShouldContain("_INBOX.");
catchupInbox.ShouldContain("_INBOX.CATCHUP.");
var sub = raft.Subscribe("raft.append");
raft.AeSub.ShouldNotBeNull();
raft.Unsubscribe(sub);
raft.AeSub.ShouldBeNull();
raft.CreateInternalSubs().ShouldBeNull();
raft.ResetElectionTimeout();
raft.Elect.ShouldNotBeNull();
raft.ResetElect(TimeSpan.FromMilliseconds(10));
raft.ElectTimer().ShouldBeGreaterThan(DateTime.MinValue);
}
[Fact]
public void CodecHelpers_WhenRoundTrippingEntriesAndVotes_ShouldPreserveFields()
{
var raft = new Raft
{
GroupName = "RG",
};
var entry = raft.NewEntry(EntryType.EntryNormal, [1, 2, 3]);
var proposed = raft.NewProposedEntry(entry, "reply");
proposed.Reply.ShouldBe("reply");
var appendEntry = raft.NewAppendEntry("L1", 2, 1, 1, 0, [entry]);
appendEntry.String().ShouldContain("leader=L1");
appendEntry.ShouldStore().ShouldBeTrue();
var decodedAppend = raft.DecodeAppendEntry(appendEntry.Encode());
decodedAppend.Leader.ShouldBe("L1");
var appendResponse = raft.NewAppendEntryResponse(2, 1, "N2", "_R_", success: true);
var decodedResponse = raft.DecodeAppendEntryResponse(appendResponse.Encode());
decodedResponse.Success.ShouldBeTrue();
decodedResponse.Peer.ShouldBe("N2");
var voteRequest = new VoteRequest { TermV = 4, Candidate = "N3", LastIndex = 9, LastTerm = 3, Reply = "_R_" };
var decodedVoteRequest = raft.DecodeVoteRequest(voteRequest.Encode());
decodedVoteRequest.Candidate.ShouldBe("N3");
var voteResponse = new VoteResponse { TermV = 4, Peer = "N2", Granted = true, Empty = false };
var decodedVoteResponse = raft.DecodeVoteResponse(voteResponse.Encode());
decodedVoteResponse.Granted.ShouldBeTrue();
decodedVoteResponse.Peer.ShouldBe("N2");
}
[Fact]
public void PeerStatePersistence_WhenWrittenAndRead_ShouldRoundTrip()
{
var raft = new Raft();
var storeDir = Path.Combine(Path.GetTempPath(), $"raft-peer-state-{Guid.NewGuid():N}");
try
{
var state = new PeerState
{
KnownPeers = ["A1", "A2"],
ClusterSize = 2,
DomainExt = 7,
};
raft.PeerStateBufSize(state).ShouldBeGreaterThan(0);
raft.WritePeerStateStatic(storeDir, state).ShouldBeNull();
var (readState, readError) = raft.ReadPeerState(storeDir);
readError.ShouldBeNull();
readState.ShouldNotBeNull();
readState!.KnownPeers.Count.ShouldBe(2);
raft.WriteTermVoteStatic(storeDir, 6, "A1").ShouldBeNull();
raft.Term_.ShouldBe(6UL);
raft.Vote.ShouldBe("A1");
}
finally
{
if (Directory.Exists(storeDir))
{
Directory.Delete(storeDir, recursive: true);
}
}
}
}