Files
natsdotnet/tests/NATS.Server.JetStream.Tests/JetStream/Storage/SequenceSetTests.cs
Joseph Doherty 78b4bc2486 refactor: extract NATS.Server.JetStream.Tests project
Move 225 JetStream-related test files from NATS.Server.Tests into a
dedicated NATS.Server.JetStream.Tests project. This includes root-level
JetStream*.cs files, storage test files (FileStore, MemStore,
StreamStoreContract), and the full JetStream/ subfolder tree (Api,
Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams).

Updated all namespaces, added InternalsVisibleTo, registered in the
solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
2026-03-12 15:58:10 -04:00

396 lines
12 KiB
C#

// Reference: golang/nats-server/server/avl/seqset_test.go
// Tests ported / inspired by:
// TestSequenceSetBasic → Add_Contains_Count_BasicOperations
// TestSequenceSetRange → GetEnumerator_ReturnsAscendingOrder
// TestSequenceSetDelete → Remove_SplitsAndTrimsRanges
// (range compression) → Add_ContiguousSequences_CompressesToOneRange
// (binary search) → Contains_BinarySearchCorrectness
// (boundary) → Add_Remove_AtBoundaries
using NATS.Server.JetStream.Storage;
namespace NATS.Server.JetStream.Tests.JetStream.Storage;
/// <summary>
/// Unit tests for <see cref="SequenceSet"/> — the range-compressed sorted set
/// used to track soft-deleted sequences in JetStream FileStore blocks.
///
/// Reference: golang/nats-server/server/avl/seqset_test.go
/// </summary>
public sealed class SequenceSetTests
{
// -------------------------------------------------------------------------
// Basic Add / Contains / Count
// -------------------------------------------------------------------------
// Go: TestSequenceSetBasic — empty set has zero count
[Fact]
public void Count_EmptySet_ReturnsZero()
{
var ss = new SequenceSet();
ss.Count.ShouldBe(0);
ss.IsEmpty.ShouldBeTrue();
}
// Go: TestSequenceSetBasic — single element is found
[Fact]
public void Add_SingleSequence_ContainsIt()
{
var ss = new SequenceSet();
ss.Add(42).ShouldBeTrue();
ss.Contains(42).ShouldBeTrue();
ss.Count.ShouldBe(1);
ss.IsEmpty.ShouldBeFalse();
}
// Go: duplicate insert returns false (already present)
[Fact]
public void Add_DuplicateSequence_ReturnsFalse()
{
var ss = new SequenceSet();
ss.Add(10).ShouldBeTrue();
ss.Add(10).ShouldBeFalse();
ss.Count.ShouldBe(1);
}
// Go: non-member returns false on Contains
[Fact]
public void Contains_NonMember_ReturnsFalse()
{
var ss = new SequenceSet();
ss.Add(5);
ss.Contains(4).ShouldBeFalse();
ss.Contains(6).ShouldBeFalse();
ss.Contains(0).ShouldBeFalse();
ss.Contains(ulong.MaxValue).ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Range compression
// -------------------------------------------------------------------------
// Adding three contiguous sequences should compress to a single range.
// This is the key efficiency property of SequenceSet vs HashSet.
[Fact]
public void Add_ContiguousSequences_CompressesToOneRange()
{
var ss = new SequenceSet();
ss.Add(1);
ss.Add(2);
ss.Add(3);
ss.Count.ShouldBe(3);
ss.RangeCount.ShouldBe(1); // single range [1, 3]
ss.Contains(1).ShouldBeTrue();
ss.Contains(2).ShouldBeTrue();
ss.Contains(3).ShouldBeTrue();
}
// Adding in reverse order should still compress.
[Fact]
public void Add_ContiguousReverse_CompressesToOneRange()
{
var ss = new SequenceSet();
ss.Add(3);
ss.Add(2);
ss.Add(1);
ss.Count.ShouldBe(3);
ss.RangeCount.ShouldBe(1); // single range [1, 3]
}
// Two separate gaps should stay as two ranges.
[Fact]
public void Add_WithGap_TwoRanges()
{
var ss = new SequenceSet();
ss.Add(1);
ss.Add(2);
ss.Add(4); // gap at 3
ss.Add(5);
ss.Count.ShouldBe(4);
ss.RangeCount.ShouldBe(2); // [1,2] and [4,5]
}
// Filling the gap merges to one range.
[Fact]
public void Add_FillsGap_MergesToOneRange()
{
var ss = new SequenceSet();
ss.Add(1);
ss.Add(2);
ss.Add(4);
ss.Add(5);
ss.RangeCount.ShouldBe(2);
// Fill the gap.
ss.Add(3);
ss.RangeCount.ShouldBe(1); // [1, 5]
ss.Count.ShouldBe(5);
}
// Large run of contiguous sequences stays as one range.
[Fact]
public void Add_LargeContiguousRun_OnlyOneRange()
{
var ss = new SequenceSet();
for (ulong i = 1; i <= 10_000; i++)
ss.Add(i);
ss.Count.ShouldBe(10_000);
ss.RangeCount.ShouldBe(1);
}
// -------------------------------------------------------------------------
// Remove / split / trim
// -------------------------------------------------------------------------
// Removing from an empty set returns false.
[Fact]
public void Remove_EmptySet_ReturnsFalse()
{
var ss = new SequenceSet();
ss.Remove(1).ShouldBeFalse();
}
// Removing a non-member returns false and doesn't change count.
[Fact]
public void Remove_NonMember_ReturnsFalse()
{
var ss = new SequenceSet();
ss.Add(5);
ss.Remove(4).ShouldBeFalse();
ss.Count.ShouldBe(1);
}
// Removing the only element empties the set.
[Fact]
public void Remove_SingleElement_EmptiesSet()
{
var ss = new SequenceSet();
ss.Add(7);
ss.Remove(7).ShouldBeTrue();
ss.Count.ShouldBe(0);
ss.IsEmpty.ShouldBeTrue();
ss.Contains(7).ShouldBeFalse();
}
// Removing the left edge of a range trims it.
[Fact]
public void Remove_LeftEdge_TrimsRange()
{
var ss = new SequenceSet();
ss.Add(1); ss.Add(2); ss.Add(3);
ss.RangeCount.ShouldBe(1);
ss.Remove(1).ShouldBeTrue();
ss.Count.ShouldBe(2);
ss.Contains(1).ShouldBeFalse();
ss.Contains(2).ShouldBeTrue();
ss.Contains(3).ShouldBeTrue();
ss.RangeCount.ShouldBe(1); // still one range [2, 3]
}
// Removing the right edge of a range trims it.
[Fact]
public void Remove_RightEdge_TrimsRange()
{
var ss = new SequenceSet();
ss.Add(1); ss.Add(2); ss.Add(3);
ss.Remove(3).ShouldBeTrue();
ss.Count.ShouldBe(2);
ss.Contains(3).ShouldBeFalse();
ss.RangeCount.ShouldBe(1); // still [1, 2]
}
// Removing the middle element splits a range into two.
[Fact]
public void Remove_MiddleElement_SplitsRange()
{
var ss = new SequenceSet();
ss.Add(1); ss.Add(2); ss.Add(3); ss.Add(4); ss.Add(5);
ss.RangeCount.ShouldBe(1);
ss.Remove(3).ShouldBeTrue();
ss.Count.ShouldBe(4);
ss.Contains(3).ShouldBeFalse();
ss.Contains(1).ShouldBeTrue();
ss.Contains(2).ShouldBeTrue();
ss.Contains(4).ShouldBeTrue();
ss.Contains(5).ShouldBeTrue();
ss.RangeCount.ShouldBe(2); // [1,2] and [4,5]
}
// -------------------------------------------------------------------------
// Enumeration
// -------------------------------------------------------------------------
// GetEnumerator returns all sequences in ascending order.
[Fact]
public void GetEnumerator_ReturnsAscendingOrder()
{
var ss = new SequenceSet();
ss.Add(5); ss.Add(3); ss.Add(1); ss.Add(2); ss.Add(4);
var list = ss.ToList();
list.ShouldBe([1, 2, 3, 4, 5]);
}
// Enumeration over a compressed range expands correctly.
[Fact]
public void GetEnumerator_CompressedRange_ExpandsAll()
{
var ss = new SequenceSet();
for (ulong i = 100; i <= 200; i++)
ss.Add(i);
var list = ss.ToList();
list.Count.ShouldBe(101);
list[0].ShouldBe(100UL);
list[^1].ShouldBe(200UL);
}
// Enumeration over multiple disjoint ranges returns all in order.
[Fact]
public void GetEnumerator_MultipleRanges_AllInOrder()
{
var ss = new SequenceSet();
ss.Add(10); ss.Add(11);
ss.Add(20); ss.Add(21); ss.Add(22);
ss.Add(30);
var list = ss.ToList();
list.ShouldBe([10UL, 11UL, 20UL, 21UL, 22UL, 30UL]);
}
// -------------------------------------------------------------------------
// Clear
// -------------------------------------------------------------------------
[Fact]
public void Clear_RemovesAll()
{
var ss = new SequenceSet();
ss.Add(1); ss.Add(2); ss.Add(3);
ss.Clear();
ss.Count.ShouldBe(0);
ss.IsEmpty.ShouldBeTrue();
ss.Contains(1).ShouldBeFalse();
}
// -------------------------------------------------------------------------
// ToHashSet snapshot
// -------------------------------------------------------------------------
[Fact]
public void ToHashSet_ReturnsAllElements()
{
var ss = new SequenceSet();
ss.Add(1); ss.Add(2); ss.Add(5); ss.Add(6); ss.Add(7);
var hs = ss.ToHashSet();
hs.Count.ShouldBe(5);
hs.ShouldContain(1UL);
hs.ShouldContain(2UL);
hs.ShouldContain(5UL);
hs.ShouldContain(6UL);
hs.ShouldContain(7UL);
}
// -------------------------------------------------------------------------
// Binary search correctness — sparse insertions
// -------------------------------------------------------------------------
// Reference: Go seqset_test.go — large number of non-contiguous sequences.
[Fact]
public void Add_Contains_SparseInsertions_AllFound()
{
var ss = new SequenceSet();
var expected = new List<ulong>();
for (ulong i = 1; i <= 1000; i += 3) // every 3rd: 1, 4, 7, ...
{
ss.Add(i);
expected.Add(i);
}
ss.Count.ShouldBe(expected.Count);
foreach (var seq in expected)
ss.Contains(seq).ShouldBeTrue($"Expected seq {seq} to be present");
// Non-members should not appear.
ss.Contains(2).ShouldBeFalse();
ss.Contains(3).ShouldBeFalse();
ss.Contains(999).ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Boundary conditions
// -------------------------------------------------------------------------
[Fact]
public void Add_SequenceZero_Works()
{
var ss = new SequenceSet();
ss.Add(0).ShouldBeTrue();
ss.Contains(0).ShouldBeTrue();
ss.Count.ShouldBe(1);
}
[Fact]
public void Add_AdjacentToZero_Merges()
{
var ss = new SequenceSet();
ss.Add(0);
ss.Add(1);
ss.RangeCount.ShouldBe(1); // [0, 1]
ss.Count.ShouldBe(2);
}
[Fact]
public void Add_Remove_RoundTrip()
{
var ss = new SequenceSet();
for (ulong i = 1; i <= 100; i++)
ss.Add(i);
// Remove all odd sequences.
for (ulong i = 1; i <= 100; i += 2)
ss.Remove(i);
ss.Count.ShouldBe(50);
for (ulong i = 2; i <= 100; i += 2)
ss.Contains(i).ShouldBeTrue();
for (ulong i = 1; i <= 99; i += 2)
ss.Contains(i).ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Merging at boundaries of existing ranges (not just single adjacency)
// -------------------------------------------------------------------------
[Fact]
public void Add_BridgesMultipleGaps_CorrectState()
{
var ss = new SequenceSet();
// Create three separate ranges: [1,2], [4,5], [7,8]
ss.Add(1); ss.Add(2);
ss.Add(4); ss.Add(5);
ss.Add(7); ss.Add(8);
ss.RangeCount.ShouldBe(3);
ss.Count.ShouldBe(6);
// Fill gap between [1,2] and [4,5]: add 3
ss.Add(3);
ss.RangeCount.ShouldBe(2); // [1,5] and [7,8]
ss.Count.ShouldBe(7);
// Fill gap between [1,5] and [7,8]: add 6
ss.Add(6);
ss.RangeCount.ShouldBe(1); // [1,8]
ss.Count.ShouldBe(8);
}
}