Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions src/Celerity.Tests/Collections/CeleritySetCollisionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using Celerity.Collections;
using Celerity.Hashing;

namespace Celerity.Tests.Collections;

/// <summary>
/// Tests that exercise <see cref="CeleritySet{T, THasher}"/>
/// under maximum hash collision pressure and with reference-type elements
/// whose default is <c>null</c>.
/// </summary>
public class CeleritySetCollisionTests
{
private struct ConstantIntHasher : IHashProvider<int>
{
public int Hash(int key) => 42;
}

private struct ConstantStringHasher : IHashProvider<string>
{
public int Hash(string key) => 7;
}

// ---------------------------------------------------------------
// Int-element collision tests
// ---------------------------------------------------------------

[Fact]
public void Insert_ShouldSucceed_UnderFullCollision()
{
var set = new CeleritySet<int, ConstantIntHasher>(16);
for (int i = 1; i <= 10; i++)
set.Add(i);

Assert.Equal(10, set.Count);
for (int i = 1; i <= 10; i++)
Assert.True(set.Contains(i));
}

[Fact]
public void Duplicate_ShouldBeRejected_UnderFullCollision()
{
var set = new CeleritySet<int, ConstantIntHasher>(16);
for (int i = 1; i <= 5; i++)
set.Add(i);
for (int i = 1; i <= 5; i++)
Assert.False(set.TryAdd(i));

Assert.Equal(5, set.Count);
}

[Fact]
public void Remove_ShouldRehashCluster_UnderFullCollision()
{
var set = new CeleritySet<int, ConstantIntHasher>(16);
for (int i = 1; i <= 6; i++)
set.Add(i);

Assert.True(set.Remove(3));
Assert.Equal(5, set.Count);
Assert.False(set.Contains(3));

Assert.True(set.Contains(1));
Assert.True(set.Contains(2));
Assert.True(set.Contains(4));
Assert.True(set.Contains(5));
Assert.True(set.Contains(6));
}

[Fact]
public void RemoveThenReinsert_ShouldWork_UnderFullCollision()
{
var set = new CeleritySet<int, ConstantIntHasher>(8);

for (int i = 1; i <= 10; i++)
set.Add(i);

for (int i = 1; i <= 10; i += 2)
Assert.True(set.Remove(i));

Assert.Equal(5, set.Count);

for (int i = 1; i <= 10; i += 2)
set.Add(i);

Assert.Equal(10, set.Count);
for (int i = 1; i <= 10; i++)
Assert.True(set.Contains(i));
}

[Fact]
public void DefaultKey_ShouldWorkAlongside_CollisionChain()
{
var set = new CeleritySet<int, ConstantIntHasher>(16);
set.Add(0);
for (int i = 1; i <= 5; i++)
set.Add(i);

Assert.Equal(6, set.Count);
Assert.True(set.Contains(0));
for (int i = 1; i <= 5; i++)
Assert.True(set.Contains(i));

Assert.True(set.Remove(0));
Assert.Equal(5, set.Count);
Assert.False(set.Contains(0));
for (int i = 1; i <= 5; i++)
Assert.True(set.Contains(i));
}

[Fact]
public void Resize_ShouldPreserveAll_UnderFullCollision()
{
var set = new CeleritySet<int, ConstantIntHasher>(
capacity: 4, loadFactor: 0.5f);

for (int i = 1; i <= 20; i++)
set.Add(i);

Assert.Equal(20, set.Count);
for (int i = 1; i <= 20; i++)
Assert.True(set.Contains(i));
}

// ---------------------------------------------------------------
// String elements — exercises default(string) == null path
// ---------------------------------------------------------------

[Fact]
public void StringNull_ShouldRoundTrip()
{
var set = new CeleritySet<string, ConstantStringHasher>();
set.Add(null!);

Assert.True(set.Contains(null!));
Assert.Equal(1, set.Count);
}

[Fact]
public void StringNull_ShouldCoexistWithNonNullElements()
{
var set = new CeleritySet<string, ConstantStringHasher>(16);
set.Add(null!);
set.Add("alpha");
set.Add("beta");
set.Add("gamma");

Assert.Equal(4, set.Count);
Assert.True(set.Contains(null!));
Assert.True(set.Contains("alpha"));
Assert.True(set.Contains("beta"));
Assert.True(set.Contains("gamma"));
}

[Fact]
public void StringNull_Remove_ShouldWork()
{
var set = new CeleritySet<string, ConstantStringHasher>(16);
set.Add(null!);
set.Add("a");

Assert.True(set.Remove(null!));
Assert.False(set.Contains(null!));
Assert.Equal(1, set.Count);
Assert.True(set.Contains("a"));
}

[Fact]
public void StringNull_Clear_ShouldReset()
{
var set = new CeleritySet<string, ConstantStringHasher>();
set.Add(null!);
set.Add("x");

set.Clear();

Assert.Equal(0, set.Count);
Assert.False(set.Contains(null!));
Assert.False(set.Contains("x"));

// Reusable after clear.
set.Add(null!);
Assert.Equal(1, set.Count);
Assert.True(set.Contains(null!));
}
}
183 changes: 183 additions & 0 deletions src/Celerity.Tests/Collections/CeleritySetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using Celerity.Collections;
using Celerity.Hashing;

namespace Celerity.Tests.Collections;

public class CeleritySetTests
{
[Fact]
public void TryAdd_ShouldAddAndContain()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
Assert.True(set.TryAdd(10));
Assert.True(set.Contains(10));
Assert.Equal(1, set.Count);
}

[Fact]
public void TryAdd_ShouldReturnFalse_WhenDuplicate()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
Assert.True(set.TryAdd(10));
Assert.False(set.TryAdd(10));
Assert.Equal(1, set.Count);
}

[Fact]
public void Add_ShouldThrow_WhenDuplicate()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
set.Add(10);
Assert.Throws<ArgumentException>(() => set.Add(10));
}

[Fact]
public void Contains_ShouldReturnFalse_WhenNotPresent()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
Assert.False(set.Contains(99));
}

[Fact]
public void Remove_ShouldDeleteElement()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
set.Add(7);

Assert.True(set.Remove(7));
Assert.False(set.Contains(7));
Assert.Equal(0, set.Count);
}

[Fact]
public void Remove_ShouldReturnFalse_WhenNotPresent()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
Assert.False(set.Remove(7));
}

[Fact]
public void Set_ShouldResize_WhenThresholdExceeded()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>(4);
set.Add(1);
set.Add(2);
set.Add(3);
set.Add(4); // Triggers resize

Assert.Equal(4, set.Count);
Assert.True(set.Contains(1));
Assert.True(set.Contains(2));
Assert.True(set.Contains(3));
Assert.True(set.Contains(4));
}

// Regression: default(TKey) collides with the "empty slot" sentinel.
[Fact]
public void TryAdd_ShouldHandleZeroIntKey()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
Assert.True(set.TryAdd(0));
Assert.True(set.Contains(0));
Assert.Equal(1, set.Count);
}

[Fact]
public void TryAdd_ZeroIntKey_ShouldReturnFalse_WhenDuplicate()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
Assert.True(set.TryAdd(0));
Assert.False(set.TryAdd(0));
Assert.Equal(1, set.Count);
}

[Fact]
public void Remove_ShouldHandleZeroIntKey()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
set.Add(0);
set.Add(1);

Assert.True(set.Remove(0));
Assert.False(set.Contains(0));
Assert.True(set.Contains(1));
Assert.Equal(1, set.Count);
Assert.False(set.Remove(0));
}

[Fact]
public void DefaultKey_ShouldSurviveResize()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>(4);
set.Add(0);
set.Add(1);
set.Add(2);
set.Add(3);
set.Add(4); // Triggers resize while the default-key entry is live.

Assert.Equal(5, set.Count);
Assert.True(set.Contains(0));
Assert.True(set.Contains(1));
Assert.True(set.Contains(4));
}

[Fact]
public void Clear_ShouldRemoveAllElements()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>();
for (int i = 0; i < 32; i++)
set.Add(i);

set.Clear();

Assert.Equal(0, set.Count);
for (int i = 0; i < 32; i++)
Assert.False(set.Contains(i));

// Reusable after clear.
set.Add(0);
set.Add(5);
Assert.Equal(2, set.Count);
Assert.True(set.Contains(0));
Assert.True(set.Contains(5));
}

// Guid.Empty == default(Guid)
[Fact]
public void GuidEmpty_ShouldRoundTrip()
{
var set = new CeleritySet<Guid, GuidIdentityHasher>();
set.Add(Guid.Empty);
var other = Guid.NewGuid();
set.Add(other);

Assert.True(set.Contains(Guid.Empty));
Assert.True(set.Contains(other));
Assert.Equal(2, set.Count);
}

[Fact]
public void RemoveThenReinsert_ManyElements()
{
var set = new CeleritySet<int, Int32WangNaiveHasher>(8);
for (int i = 1; i <= 100; i++)
set.Add(i);

for (int i = 1; i <= 100; i += 2)
Assert.True(set.Remove(i));

Assert.Equal(50, set.Count);

for (int i = 1; i <= 100; i += 2)
set.Add(i);

Assert.Equal(100, set.Count);
for (int i = 1; i <= 100; i++)
Assert.True(set.Contains(i));
}

private struct GuidIdentityHasher : IHashProvider<Guid>
{
public int Hash(Guid key) => key.GetHashCode();
}
}
Loading
Loading