// 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; /// /// Unit tests for — the range-compressed sorted set /// used to track soft-deleted sequences in JetStream FileStore blocks. /// /// Reference: golang/nats-server/server/avl/seqset_test.go /// 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(); 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); } }