393 lines
12 KiB
C#
393 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using Shouldly;
|
|
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
|
|
|
namespace ZB.MOM.NatsNet.Server.Tests.Internal.DataStructures;
|
|
|
|
public sealed class SequenceSetTests
|
|
{
|
|
private static readonly int NumEntries = SequenceSet.NumEntries; // 2048
|
|
|
|
// --- Basic operations ---
|
|
|
|
[Fact]
|
|
public void SeqSetBasics_ShouldSucceed()
|
|
{
|
|
var ss = new SequenceSet();
|
|
var seqs = new ulong[] { 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 duplicate
|
|
var (lh, rh) = ss.Heights();
|
|
lh.ShouldBe(0);
|
|
rh.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetLeftLean_ShouldSucceed()
|
|
{
|
|
var ss = new SequenceSet();
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetRightLean_ShouldSucceed()
|
|
{
|
|
var ss = new SequenceSet();
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetCorrectness_ShouldSucceed()
|
|
{
|
|
var num = 100_000;
|
|
var maxVal = 500_000;
|
|
var rng = new Random(42);
|
|
|
|
var reference = new HashSet<ulong>(num);
|
|
var ss = new SequenceSet();
|
|
|
|
for (var i = 0; i < num; i++)
|
|
{
|
|
var n = (ulong)rng.NextInt64(maxVal + 1);
|
|
ss.Insert(n);
|
|
reference.Add(n);
|
|
}
|
|
|
|
for (var i = 0UL; i <= (ulong)maxVal; i++)
|
|
ss.Exists(i).ShouldBe(reference.Contains(i));
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetRange_ShouldSucceed()
|
|
{
|
|
var num = 2 * NumEntries + 22;
|
|
var nums = new List<ulong>(num);
|
|
for (var i = 0; i < num; i++)
|
|
nums.Add((ulong)i);
|
|
|
|
var rng = new Random(42);
|
|
for (var i = nums.Count - 1; i > 0; i--)
|
|
{
|
|
var j = rng.Next(i + 1);
|
|
(nums[i], nums[j]) = (nums[j], nums[i]);
|
|
}
|
|
|
|
var ss = new SequenceSet();
|
|
foreach (var n in nums)
|
|
ss.Insert(n);
|
|
|
|
var collected = new List<ulong>();
|
|
ss.Range(n => { collected.Add(n); return true; });
|
|
collected.Count.ShouldBe(num);
|
|
for (var i = 0; i < num; i++)
|
|
collected[i].ShouldBe((ulong)i);
|
|
|
|
// Test early termination.
|
|
collected.Clear();
|
|
ss.Range(n =>
|
|
{
|
|
if (n >= 10) return false;
|
|
collected.Add(n);
|
|
return true;
|
|
});
|
|
collected.Count.ShouldBe(10);
|
|
for (var i = 0UL; i < 10; i++)
|
|
collected[(int)i].ShouldBe(i);
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetDelete_ShouldSucceed()
|
|
{
|
|
var ss = new SequenceSet();
|
|
var seqs = new ulong[] { 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();
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetInsertAndDeletePedantic_ShouldSucceed()
|
|
{
|
|
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);
|
|
for (var i = nums.Count - 1; i > 0; i--)
|
|
{
|
|
var j = rng.Next(i + 1);
|
|
(nums[i], nums[j]) = (nums[j], nums[i]);
|
|
}
|
|
|
|
void AssertBalanced()
|
|
{
|
|
SequenceSet.Node.NodeIter(ss.Root, n =>
|
|
{
|
|
if (n != null && n.Height != (SequenceSet.Node.BalanceFactor(n) == int.MinValue ? 0 : 0))
|
|
{
|
|
// Height check: verify height equals max child height + 1
|
|
var expectedH = 1 + Math.Max(n.Left?.Height ?? 0, n.Right?.Height ?? 0);
|
|
n.Height.ShouldBe(expectedH);
|
|
}
|
|
});
|
|
|
|
var bf = SequenceSet.Node.BalanceFactor(ss.Root);
|
|
(bf is >= -1 and <= 1).ShouldBeTrue();
|
|
}
|
|
|
|
foreach (var n in nums)
|
|
{
|
|
ss.Insert(n);
|
|
AssertBalanced();
|
|
}
|
|
ss.Root.ShouldNotBeNull();
|
|
|
|
foreach (var n in nums)
|
|
{
|
|
ss.Delete(n);
|
|
AssertBalanced();
|
|
ss.Exists(n).ShouldBeFalse();
|
|
if (ss.Size > 0)
|
|
ss.Root.ShouldNotBeNull();
|
|
}
|
|
ss.Root.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetMinMax_ShouldSucceed()
|
|
{
|
|
var ss = new SequenceSet();
|
|
var seqs = new ulong[] { 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);
|
|
|
|
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);
|
|
for (var i = nums.Count - 1; i > 0; i--)
|
|
{
|
|
var j = rng.Next(i + 1);
|
|
(nums[i], nums[j]) = (nums[j], nums[i]);
|
|
}
|
|
foreach (var n in nums)
|
|
ss.Insert(n);
|
|
|
|
(min, max) = ss.MinMax();
|
|
min.ShouldBe(0UL);
|
|
max.ShouldBe((ulong)(num - 1));
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetClone_ShouldSucceed()
|
|
{
|
|
var num = 100_000;
|
|
var maxVal = 500_000;
|
|
var rng = new Random(42);
|
|
|
|
var ss = new SequenceSet();
|
|
for (var i = 0; i < num; i++)
|
|
ss.Insert((ulong)rng.NextInt64(maxVal + 1));
|
|
|
|
var ssc = ss.Clone();
|
|
ssc.Size.ShouldBe(ss.Size);
|
|
ssc.Nodes.ShouldBe(ss.Nodes);
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetUnion_ShouldSucceed()
|
|
{
|
|
var ss1 = new SequenceSet();
|
|
var seqs1 = new ulong[] { 22, 222, 2222, 2, 2, 4 };
|
|
foreach (var seq in seqs1) ss1.Insert(seq);
|
|
|
|
var ss2 = new SequenceSet();
|
|
var seqs2 = new ulong[] { 33, 333, 3333, 3, 33_333, 333_333 };
|
|
foreach (var seq in seqs2) ss2.Insert(seq);
|
|
|
|
var ss = SequenceSet.UnionSets(ss1, ss2);
|
|
ss.ShouldNotBeNull();
|
|
ss!.Size.ShouldBe(11);
|
|
|
|
foreach (var n in seqs1) ss.Exists(n).ShouldBeTrue();
|
|
foreach (var n in seqs2) ss.Exists(n).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetFirst_ShouldSucceed()
|
|
{
|
|
var seqs = new ulong[] { 22, 222, 2222, 222_222 };
|
|
foreach (var seq in seqs)
|
|
{
|
|
var ss = new SequenceSet();
|
|
ss.Insert(seq);
|
|
ss.Root.ShouldNotBeNull();
|
|
ss.Root!.Base.ShouldBe((seq / (ulong)NumEntries) * (ulong)NumEntries);
|
|
|
|
ss.Empty();
|
|
ss.SetInitialMin(seq);
|
|
ss.Insert(seq);
|
|
ss.Root.ShouldNotBeNull();
|
|
ss.Root!.Base.ShouldBe(seq);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetDistinctUnion_ShouldSucceed()
|
|
{
|
|
var ss1 = new SequenceSet();
|
|
var seqs1 = new ulong[] { 1, 10, 100, 200 };
|
|
foreach (var seq in seqs1) ss1.Insert(seq);
|
|
|
|
var ss2 = new SequenceSet();
|
|
var seqs2 = new ulong[] { 5000, 6100, 6200, 6222 };
|
|
foreach (var seq in seqs2) ss2.Insert(seq);
|
|
|
|
var ss = ss1.Clone();
|
|
ss.Union(ss2);
|
|
|
|
var allSeqs = new List<ulong>(seqs1);
|
|
allSeqs.AddRange(seqs2);
|
|
|
|
ss.Size.ShouldBe(allSeqs.Count);
|
|
foreach (var seq in allSeqs)
|
|
ss.Exists(seq).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void SeqSetDecodeV1_ShouldSucceed()
|
|
{
|
|
var seqs = new ulong[] { 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();
|
|
}
|
|
|
|
// --- Encode/Decode round-trip ---
|
|
|
|
[Fact]
|
|
public void SeqSetEncodeDecode_RoundTrip_ShouldSucceed()
|
|
{
|
|
var num = 2_500_000;
|
|
var maxVal = 5_000_000;
|
|
var rng = new Random(42);
|
|
|
|
var reference = new HashSet<ulong>(num);
|
|
var ss = new SequenceSet();
|
|
for (var i = 0; i < num; i++)
|
|
{
|
|
var n = (ulong)rng.NextInt64(maxVal + 1);
|
|
ss.Insert(n);
|
|
reference.Add(n);
|
|
}
|
|
|
|
var buf = ss.Encode(null);
|
|
var (ss2, _) = SequenceSet.Decode(buf);
|
|
|
|
ss2.Nodes.ShouldBe(ss.Nodes);
|
|
ss2.Size.ShouldBe(ss.Size);
|
|
}
|
|
|
|
// --- Performance / scale tests (no strict timing assertions) ---
|
|
|
|
[Fact]
|
|
public void NoRaceSeqSetSizeComparison_ShouldSucceed()
|
|
{
|
|
// Insert 5M items; verify correctness (memory comparison is GC-managed, skip strict thresholds)
|
|
var num = 5_000_000;
|
|
var maxVal = 7_000_000;
|
|
var rng = new Random(42);
|
|
|
|
var ss = new SequenceSet();
|
|
var reference = new HashSet<ulong>(num);
|
|
for (var i = 0; i < num; i++)
|
|
{
|
|
var n = (ulong)rng.NextInt64(maxVal + 1);
|
|
ss.Insert(n);
|
|
reference.Add(n);
|
|
}
|
|
|
|
ss.Size.ShouldBe(reference.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoRaceSeqSetEncodeLarge_ShouldSucceed()
|
|
{
|
|
var num = 2_500_000;
|
|
var maxVal = 5_000_000;
|
|
var rng = new Random(42);
|
|
|
|
var ss = new SequenceSet();
|
|
for (var i = 0; i < num; i++)
|
|
ss.Insert((ulong)rng.NextInt64(maxVal + 1));
|
|
|
|
var buf = ss.Encode(null);
|
|
var (ss2, _) = SequenceSet.Decode(buf);
|
|
|
|
ss2.Nodes.ShouldBe(ss.Nodes);
|
|
ss2.Size.ShouldBe(ss.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoRaceSeqSetRelativeSpeed_ShouldSucceed()
|
|
{
|
|
// Correctness: all inserted items must be findable.
|
|
// Timing assertions are omitted — performance is validated via BenchmarkDotNet benchmarks.
|
|
var num = 1_000_000;
|
|
var maxVal = 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(maxVal + 1);
|
|
|
|
var ss = new SequenceSet();
|
|
foreach (var n in seqs) ss.Insert(n);
|
|
foreach (var n in seqs) ss.Exists(n).ShouldBeTrue();
|
|
}
|
|
}
|