Files
natsdotnet/tests/NATS.Server.Raft.Tests/Raft/RaftWalTests.cs
Joseph Doherty edf9ed770e refactor: extract NATS.Server.Raft.Tests project
Move 43 Raft consensus test files (8 root-level + 35 in Raft/ subfolder)
from NATS.Server.Tests into a dedicated NATS.Server.Raft.Tests project.
Update namespaces, add InternalsVisibleTo, and fix timing/exception
handling issues in moved test files.
2026-03-12 15:36:02 -04:00

148 lines
4.6 KiB
C#

using NATS.Server.Raft;
// Go reference: server/raft.go (WAL binary format, compaction, CRC integrity)
namespace NATS.Server.Raft.Tests.Raft;
public class RaftWalTests : IDisposable
{
private readonly string _root;
public RaftWalTests()
{
_root = Path.Combine(Path.GetTempPath(), $"nats-wal-{Guid.NewGuid():N}");
Directory.CreateDirectory(_root);
}
public void Dispose()
{
if (Directory.Exists(_root))
Directory.Delete(_root, recursive: true);
}
// Go reference: server/raft.go WAL append + recover
[Fact]
public async Task Wal_persists_and_recovers_entries()
{
var walPath = Path.Combine(_root, "raft.wal");
// Write entries
{
using var wal = new RaftWal(walPath);
await wal.AppendAsync(new RaftLogEntry(1, 1, "cmd-1"));
await wal.AppendAsync(new RaftLogEntry(2, 1, "cmd-2"));
await wal.AppendAsync(new RaftLogEntry(3, 2, "cmd-3"));
await wal.SyncAsync();
}
// Recover
using var recovered = RaftWal.Load(walPath);
var entries = recovered.Entries;
entries.Count.ShouldBe(3);
entries[0].Index.ShouldBe(1);
entries[0].Term.ShouldBe(1);
entries[0].Command.ShouldBe("cmd-1");
entries[2].Index.ShouldBe(3);
entries[2].Term.ShouldBe(2);
}
// Go reference: server/raft.go compactLog
[Fact]
public async Task Wal_compact_removes_old_entries()
{
var walPath = Path.Combine(_root, "compact.wal");
using var wal = new RaftWal(walPath);
for (int i = 1; i <= 10; i++)
await wal.AppendAsync(new RaftLogEntry(i, 1, $"cmd-{i}"));
await wal.SyncAsync();
await wal.CompactAsync(5); // remove entries 1-5
using var recovered = RaftWal.Load(walPath);
recovered.Entries.Count.ShouldBe(5);
recovered.Entries.First().Index.ShouldBe(6);
}
// Go reference: server/raft.go WAL crash-truncation tolerance
[Fact]
public async Task Wal_handles_truncated_file()
{
var walPath = Path.Combine(_root, "truncated.wal");
{
using var wal = new RaftWal(walPath);
await wal.AppendAsync(new RaftLogEntry(1, 1, "good-entry"));
await wal.AppendAsync(new RaftLogEntry(2, 1, "will-be-truncated"));
await wal.SyncAsync();
}
// Truncate last few bytes to simulate crash
using (var fs = File.OpenWrite(walPath))
fs.SetLength(fs.Length - 3);
using var recovered = RaftWal.Load(walPath);
recovered.Entries.Count.ShouldBe(1);
recovered.Entries.First().Command.ShouldBe("good-entry");
}
// Go reference: server/raft.go storeMeta (term + votedFor persistence)
[Fact]
public async Task RaftNode_persists_term_and_vote()
{
var dir = Path.Combine(_root, "node-persist");
Directory.CreateDirectory(dir);
{
using var node = new RaftNode("n1", persistDirectory: dir);
node.TermState.CurrentTerm = 5;
node.TermState.VotedFor = "n2";
await node.PersistAsync(default);
}
using var recovered = new RaftNode("n1", persistDirectory: dir);
await recovered.LoadPersistedStateAsync(default);
recovered.Term.ShouldBe(5);
recovered.TermState.VotedFor.ShouldBe("n2");
}
// Go reference: server/raft.go WAL empty file edge case
[Fact]
public async Task Wal_empty_file_loads_no_entries()
{
var walPath = Path.Combine(_root, "empty.wal");
{
using var wal = new RaftWal(walPath);
await wal.SyncAsync();
}
using var recovered = RaftWal.Load(walPath);
recovered.Entries.Count.ShouldBe(0);
}
// Go reference: server/raft.go WAL CRC integrity check
[Fact]
public async Task Wal_crc_validates_record_integrity()
{
var walPath = Path.Combine(_root, "crc.wal");
{
using var wal = new RaftWal(walPath);
await wal.AppendAsync(new RaftLogEntry(1, 1, "valid"));
await wal.AppendAsync(new RaftLogEntry(2, 1, "also-valid"));
await wal.SyncAsync();
}
// Corrupt one byte in the tail of the file (inside the second record)
var bytes = File.ReadAllBytes(walPath);
bytes[^5] ^= 0xFF;
File.WriteAllBytes(walPath, bytes);
// Load should recover exactly the first record, stopping at the corrupt second
using var recovered = RaftWal.Load(walPath);
recovered.Entries.Count.ShouldBe(1);
recovered.Entries.First().Command.ShouldBe("valid");
}
}