refactor: rename remaining tests to NATS.Server.Core.Tests

- Rename tests/NATS.Server.Tests -> tests/NATS.Server.Core.Tests
- Update solution file, InternalsVisibleTo, and csproj references
- Remove JETSTREAM_INTEGRATION_MATRIX and NATS.NKeys from csproj (moved to JetStream.Tests and Auth.Tests)
- Update all namespaces from NATS.Server.Tests.* to NATS.Server.Core.Tests.*
- Replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls
- Fix stale namespace in Transport.Tests/NetworkingGoParityTests.cs
This commit is contained in:
Joseph Doherty
2026-03-12 16:14:02 -04:00
parent 78b4bc2486
commit 7fbffffd05
114 changed files with 576 additions and 1121 deletions

View File

@@ -0,0 +1,540 @@
// Copyright 2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Diagnostics;
using NATS.Server.Internal.Avl;
namespace NATS.Server.Core.Tests.Internal.Avl;
/// <summary>
/// Tests for the AVL-backed SequenceSet, ported from Go server/avl/seqset_test.go
/// and server/avl/norace_test.go.
/// </summary>
public class SequenceSetTests
{
private const int NumEntries = SequenceSet.NumEntries; // 2048
private const int BitsPerBucket = SequenceSet.BitsPerBucket;
private const int NumBuckets = SequenceSet.NumBuckets;
// Go: TestSeqSetBasics server/avl/seqset_test.go:22
[Fact]
public void Basics_InsertExistsDelete()
{
var ss = new SequenceSet();
ulong[] seqs = [22, 222, 2000, 2, 2, 4];
foreach (var seq in seqs)
{
ss.Insert(seq);
ss.Exists(seq).ShouldBeTrue();
}
ss.Nodes.ShouldBe(1);
ss.Size.ShouldBe(seqs.Length - 1); // One dup (2 appears twice)
var (lh, rh) = ss.Heights();
lh.ShouldBe(0);
rh.ShouldBe(0);
}
// Go: TestSeqSetLeftLean server/avl/seqset_test.go:38
[Fact]
public void LeftLean_TreeBalancesCorrectly()
{
var ss = new SequenceSet();
// Insert from high to low to create a left-leaning tree.
for (var i = (ulong)(4 * NumEntries); i > 0; i--)
{
ss.Insert(i);
}
ss.Nodes.ShouldBe(5);
ss.Size.ShouldBe(4 * NumEntries);
var (lh, rh) = ss.Heights();
lh.ShouldBe(2);
rh.ShouldBe(1);
}
// Go: TestSeqSetRightLean server/avl/seqset_test.go:52
[Fact]
public void RightLean_TreeBalancesCorrectly()
{
var ss = new SequenceSet();
// Insert from low to high to create a right-leaning tree.
for (var i = 0UL; i < (ulong)(4 * NumEntries); i++)
{
ss.Insert(i);
}
ss.Nodes.ShouldBe(4);
ss.Size.ShouldBe(4 * NumEntries);
var (lh, rh) = ss.Heights();
lh.ShouldBe(1);
rh.ShouldBe(2);
}
// Go: TestSeqSetCorrectness server/avl/seqset_test.go:66
[Fact]
public void Correctness_RandomInsertDelete()
{
// Generate 100k sequences across 500k range.
const int num = 100_000;
const int max = 500_000;
var rng = new Random(42);
var set = new HashSet<ulong>();
var ss = new SequenceSet();
for (var i = 0; i < num; i++)
{
var n = (ulong)rng.NextInt64(max + 1);
ss.Insert(n);
set.Add(n);
}
for (var i = 0UL; i <= max; i++)
{
ss.Exists(i).ShouldBe(set.Contains(i));
}
}
// Go: TestSeqSetRange server/avl/seqset_test.go:85
[Fact]
public void Range_IteratesInOrder()
{
var num = 2 * NumEntries + 22;
var nums = new List<ulong>(num);
for (var i = 0; i < num; i++)
{
nums.Add((ulong)i);
}
// Shuffle and insert.
var rng = new Random(42);
Shuffle(nums, rng);
var ss = new SequenceSet();
foreach (var n in nums)
{
ss.Insert(n);
}
// Range should produce ascending order.
var result = new List<ulong>();
ss.Range(n =>
{
result.Add(n);
return true;
});
result.Count.ShouldBe(num);
for (var i = 0UL; i < (ulong)num; i++)
{
result[(int)i].ShouldBe(i);
}
// Test truncating the range call.
result.Clear();
ss.Range(n =>
{
if (n >= 10)
{
return false;
}
result.Add(n);
return true;
});
result.Count.ShouldBe(10);
for (var i = 0UL; i < 10; i++)
{
result[(int)i].ShouldBe(i);
}
}
// Go: TestSeqSetDelete server/avl/seqset_test.go:123
[Fact]
public void Delete_VariousPatterns()
{
var ss = new SequenceSet();
ulong[] seqs = [22, 222, 2222, 2, 2, 4];
foreach (var seq in seqs)
{
ss.Insert(seq);
}
foreach (var seq in seqs)
{
ss.Delete(seq);
ss.Exists(seq).ShouldBeFalse();
}
ss.Root.ShouldBeNull();
}
// Go: TestSeqSetInsertAndDeletePedantic server/avl/seqset_test.go:139
[Fact]
public void InsertAndDelete_PedanticVerification()
{
var ss = new SequenceSet();
var num = 50 * NumEntries + 22;
var nums = new List<ulong>(num);
for (var i = 0; i < num; i++)
{
nums.Add((ulong)i);
}
var rng = new Random(42);
Shuffle(nums, rng);
// Insert all, verify balanced after each insert.
foreach (var n in nums)
{
ss.Insert(n);
VerifyBalanced(ss);
}
ss.Root.ShouldNotBeNull();
// Delete all, verify balanced after each delete.
foreach (var n in nums)
{
ss.Delete(n);
VerifyBalanced(ss);
ss.Exists(n).ShouldBeFalse();
if (ss.Size > 0)
{
ss.Root.ShouldNotBeNull();
}
}
ss.Root.ShouldBeNull();
}
// Go: TestSeqSetMinMax server/avl/seqset_test.go:181
[Fact]
public void MinMax_TracksCorrectly()
{
var ss = new SequenceSet();
// Simple single node.
ulong[] seqs = [22, 222, 2222, 2, 2, 4];
foreach (var seq in seqs)
{
ss.Insert(seq);
}
var (min, max) = ss.MinMax();
min.ShouldBe(2UL);
max.ShouldBe(2222UL);
// Multi-node
ss.Empty();
var num = 22 * NumEntries + 22;
var nums = new List<ulong>(num);
for (var i = 0; i < num; i++)
{
nums.Add((ulong)i);
}
var rng = new Random(42);
Shuffle(nums, rng);
foreach (var n in nums)
{
ss.Insert(n);
}
(min, max) = ss.MinMax();
min.ShouldBe(0UL);
max.ShouldBe((ulong)(num - 1));
}
// Go: TestSeqSetClone server/avl/seqset_test.go:210
[Fact]
public void Clone_IndependentCopy()
{
// Generate 100k sequences across 500k range.
const int num = 100_000;
const int max = 500_000;
var rng = new Random(42);
var ss = new SequenceSet();
for (var i = 0; i < num; i++)
{
ss.Insert((ulong)rng.NextInt64(max + 1));
}
var ssc = ss.Clone();
ssc.Size.ShouldBe(ss.Size);
ssc.Nodes.ShouldBe(ss.Nodes);
}
// Go: TestSeqSetUnion server/avl/seqset_test.go:225
[Fact]
public void Union_MergesSets()
{
var ss1 = new SequenceSet();
var ss2 = new SequenceSet();
ulong[] seqs1 = [22, 222, 2222, 2, 2, 4];
foreach (var seq in seqs1)
{
ss1.Insert(seq);
}
ulong[] seqs2 = [33, 333, 3333, 3, 33_333, 333_333];
foreach (var seq in seqs2)
{
ss2.Insert(seq);
}
var ss = SequenceSet.CreateUnion(ss1, ss2);
ss.Size.ShouldBe(11);
ulong[] allSeqs = [.. seqs1, .. seqs2];
foreach (var n in allSeqs)
{
ss.Exists(n).ShouldBeTrue();
}
}
// Go: TestSeqSetFirst server/avl/seqset_test.go:247
[Fact]
public void First_ReturnsMinimum()
{
var ss = new SequenceSet();
ulong[] seqs = [22, 222, 2222, 222_222];
foreach (var seq in seqs)
{
// Normal case where we pick first/base.
ss.Insert(seq);
ss.Root!.Base.ShouldBe((seq / (ulong)NumEntries) * (ulong)NumEntries);
ss.Empty();
// Where we set the minimum start value.
ss.SetInitialMin(seq);
ss.Insert(seq);
ss.Root!.Base.ShouldBe(seq);
ss.Empty();
}
}
// Go: TestSeqSetDistinctUnion server/avl/seqset_test.go:265
[Fact]
public void DistinctUnion_NoOverlap()
{
var ss1 = new SequenceSet();
ulong[] seqs1 = [1, 10, 100, 200];
foreach (var seq in seqs1)
{
ss1.Insert(seq);
}
var ss2 = new SequenceSet();
ulong[] seqs2 = [5000, 6100, 6200, 6222];
foreach (var seq in seqs2)
{
ss2.Insert(seq);
}
var ss = ss1.Clone();
ulong[] allSeqs = [.. seqs1, .. seqs2];
ss.Union(ss2);
ss.Size.ShouldBe(allSeqs.Length);
foreach (var seq in allSeqs)
{
ss.Exists(seq).ShouldBeTrue();
}
}
// Go: TestSeqSetDecodeV1 server/avl/seqset_test.go:289
[Fact]
public void DecodeV1_BackwardsCompatible()
{
// Encoding from v1 which was 64 buckets.
ulong[] seqs = [22, 222, 2222, 222_222, 2_222_222];
var encStr =
"FgEDAAAABQAAAABgAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADgIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA==";
var enc = Convert.FromBase64String(encStr);
var (ss, _) = SequenceSet.Decode(enc);
ss.Size.ShouldBe(seqs.Length);
foreach (var seq in seqs)
{
ss.Exists(seq).ShouldBeTrue();
}
}
// Go: TestNoRaceSeqSetSizeComparison server/avl/norace_test.go:33
[Fact]
public void SizeComparison_LargeSet()
{
// Create 5M random entries out of 7M range.
const int num = 5_000_000;
const int max = 7_000_000;
var rng = new Random(42);
var seqs = new ulong[num];
for (var i = 0; i < num; i++)
{
seqs[i] = (ulong)rng.NextInt64(max + 1);
}
// Insert into a dictionary to compare.
var dmap = new HashSet<ulong>(num);
foreach (var n in seqs)
{
dmap.Add(n);
}
// Insert into SequenceSet.
var ss = new SequenceSet();
foreach (var n in seqs)
{
ss.Insert(n);
}
// Verify sizes match.
ss.Size.ShouldBe(dmap.Count);
// Verify SequenceSet uses very few nodes relative to its element count.
// With 2048 entries per node and 7M range, we expect ~ceil(7M/2048) = ~3419 nodes at most.
ss.Nodes.ShouldBeLessThan(5000);
}
// Go: TestNoRaceSeqSetEncodeLarge server/avl/norace_test.go:81
[Fact]
public void EncodeLarge_RoundTrips()
{
const int num = 2_500_000;
const int max = 5_000_000;
var rng = new Random(42);
var ss = new SequenceSet();
for (var i = 0; i < num; i++)
{
ss.Insert((ulong)rng.NextInt64(max + 1));
}
var sw = Stopwatch.StartNew();
var buf = ss.Encode();
sw.Stop();
// Encode should be fast (the Go test uses 1ms, we allow more for .NET JIT).
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(1));
sw.Restart();
var (ss2, bytesRead) = SequenceSet.Decode(buf);
sw.Stop();
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(1));
bytesRead.ShouldBe(buf.Length);
ss2.Nodes.ShouldBe(ss.Nodes);
ss2.Size.ShouldBe(ss.Size);
}
// Go: TestNoRaceSeqSetRelativeSpeed server/avl/norace_test.go:123
[Fact]
public void RelativeSpeed_Performance()
{
const int num = 1_000_000;
const int max = 3_000_000;
var rng = new Random(42);
var seqs = new ulong[num];
for (var i = 0; i < num; i++)
{
seqs[i] = (ulong)rng.NextInt64(max + 1);
}
// SequenceSet insert.
var sw = Stopwatch.StartNew();
var ss = new SequenceSet();
foreach (var n in seqs)
{
ss.Insert(n);
}
var ssInsert = sw.Elapsed;
// SequenceSet lookup.
sw.Restart();
foreach (var n in seqs)
{
ss.Exists(n).ShouldBeTrue();
}
var ssLookup = sw.Elapsed;
// Dictionary insert.
sw.Restart();
var dmap = new HashSet<ulong>();
foreach (var n in seqs)
{
dmap.Add(n);
}
var mapInsert = sw.Elapsed;
// Dictionary lookup.
sw.Restart();
foreach (var n in seqs)
{
dmap.Contains(n).ShouldBeTrue();
}
var mapLookup = sw.Elapsed;
// Relaxed bounds: SequenceSet insert should be no more than 10x slower.
// (.NET JIT and test host overhead can be significant vs Go's simpler runtime.)
ssInsert.ShouldBeLessThan(mapInsert * 10);
ssLookup.ShouldBeLessThan(mapLookup * 10);
}
/// <summary>Verifies the AVL tree is balanced at every node.</summary>
private static void VerifyBalanced(SequenceSet ss)
{
if (ss.Root == null)
{
return;
}
// Check all node heights and balance factors.
SequenceSet.Node.NodeIter(ss.Root, n =>
{
var expectedHeight = SequenceSet.Node.MaxHeight(n) + 1;
n.Height.ShouldBe(expectedHeight, $"Node height is wrong for node with base {n.Base}");
});
var bf = SequenceSet.Node.BalanceFactor(ss.Root);
bf.ShouldBeInRange(-1, 1, "Tree is unbalanced at root");
}
/// <summary>Fisher-Yates shuffle.</summary>
private static void Shuffle(List<ulong> list, Random rng)
{
for (var i = list.Count - 1; i > 0; i--)
{
var j = rng.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
}