diff --git a/src/Orleans.Core.Abstractions/IDs/IdSpan.cs b/src/Orleans.Core.Abstractions/IDs/IdSpan.cs index b919f75f8cb..466d1963d90 100644 --- a/src/Orleans.Core.Abstractions/IDs/IdSpan.cs +++ b/src/Orleans.Core.Abstractions/IDs/IdSpan.cs @@ -94,7 +94,20 @@ private IdSpan(SerializationInfo info, StreamingContext context) public override bool Equals(object? obj) => obj is IdSpan kind && Equals(kind); /// - public bool Equals(IdSpan obj) => _value == obj._value || _value.AsSpan().SequenceEqual(obj._value); + public bool Equals(IdSpan obj) + { + if (_value == obj._value) + { + return true; + } + + if (_value is null || obj._value is null) + { + return false; + } + + return _value.AsSpan().SequenceEqual(obj._value); + } /// public override int GetHashCode() => _hashCode; @@ -139,7 +152,20 @@ public void GetObjectData(SerializationInfo info, StreamingContext context) public static byte[]? UnsafeGetArray(IdSpan id) => id._value; /// - public int CompareTo(IdSpan other) => _value.AsSpan().SequenceCompareTo(other._value.AsSpan()); + public int CompareTo(IdSpan other) + { + if (_value is null) + { + return other._value is null ? 0 : -1; + } + + if (other._value is null) + { + return 1; + } + + return _value.AsSpan().SequenceCompareTo(other._value.AsSpan()); + } /// /// Returns a string representation of this instance, decoding the value as UTF8. diff --git a/test/NonSilo.Tests/IdSpanTests.cs b/test/NonSilo.Tests/IdSpanTests.cs new file mode 100644 index 00000000000..804617ddb8c --- /dev/null +++ b/test/NonSilo.Tests/IdSpanTests.cs @@ -0,0 +1,123 @@ +using Orleans.Runtime; +using Xunit; + +namespace NonSilo.Tests +{ + /// + /// Tests for IdSpan, a primitive type for identities representing a sequence of bytes. + /// Validates equality, hash code consistency, and proper handling of null vs empty arrays. + /// + [TestCategory("BVT")] + public class IdSpanTests + { + /// + /// Tests that IdSpan.Create(string.Empty) and default(IdSpan) are NOT equal. + /// They should have different internal states (empty array vs null) and should not be considered equal. + /// + [Fact] + public void IdSpan_CreateEmptyString_NotEqualToDefault() + { + IdSpan createdFromEmptyString = IdSpan.Create(string.Empty); + IdSpan defaultIdSpan = default; + + Assert.NotEqual(createdFromEmptyString, defaultIdSpan); + Assert.False(createdFromEmptyString.Equals(defaultIdSpan)); + Assert.False(createdFromEmptyString == defaultIdSpan); + Assert.True(createdFromEmptyString != defaultIdSpan); + } + + /// + /// Tests that hash codes are consistent with equality. + /// If two IdSpans are equal, they must have the same hash code. + /// + [Fact] + public void IdSpan_HashCode_ConsistentWithEquality() + { + IdSpan id1 = IdSpan.Create("test"); + IdSpan id2 = IdSpan.Create("test"); + IdSpan id3 = IdSpan.Create("different"); + + // Equal objects must have equal hash codes + Assert.Equal(id1, id2); + Assert.Equal(id1.GetHashCode(), id2.GetHashCode()); + + // Not equal objects may have different hash codes (not required but expected) + Assert.NotEqual(id1, id3); + } + + /// + /// Tests that default IdSpan has expected properties. + /// + [Fact] + public void IdSpan_Default_HasExpectedProperties() + { + IdSpan defaultIdSpan = default; + + Assert.True(defaultIdSpan.IsDefault); + Assert.Equal(0, defaultIdSpan.GetHashCode()); + Assert.Equal("", defaultIdSpan.ToString()); + } + + /// + /// Tests that IdSpan created from empty string has expected properties. + /// + [Fact] + public void IdSpan_CreateEmptyString_HasExpectedProperties() + { + IdSpan emptyStringIdSpan = IdSpan.Create(string.Empty); + + Assert.True(emptyStringIdSpan.IsDefault); + Assert.Equal("", emptyStringIdSpan.ToString()); + // Hash code should be computed from empty byte array, not 0 + Assert.NotEqual(0, emptyStringIdSpan.GetHashCode()); + } + + /// + /// Tests that IdSpans with same content are equal. + /// + [Fact] + public void IdSpan_SameContent_AreEqual() + { + IdSpan id1 = IdSpan.Create("test123"); + IdSpan id2 = IdSpan.Create("test123"); + + Assert.Equal(id1, id2); + Assert.True(id1 == id2); + Assert.False(id1 != id2); + Assert.Equal(id1.GetHashCode(), id2.GetHashCode()); + } + + /// + /// Tests that IdSpans with different content are not equal. + /// + [Fact] + public void IdSpan_DifferentContent_AreNotEqual() + { + IdSpan id1 = IdSpan.Create("test1"); + IdSpan id2 = IdSpan.Create("test2"); + + Assert.NotEqual(id1, id2); + Assert.False(id1 == id2); + Assert.True(id1 != id2); + } + + /// + /// Tests CompareTo behavior with null and empty arrays. + /// + [Fact] + public void IdSpan_CompareTo_HandlesNullAndEmpty() + { + IdSpan defaultIdSpan = default; + IdSpan emptyStringIdSpan = IdSpan.Create(string.Empty); + IdSpan normalIdSpan = IdSpan.Create("test"); + + // Default (null) should compare differently than empty string + Assert.NotEqual(0, defaultIdSpan.CompareTo(emptyStringIdSpan)); + Assert.NotEqual(0, emptyStringIdSpan.CompareTo(defaultIdSpan)); + + // Both should be less than a normal span + Assert.True(defaultIdSpan.CompareTo(normalIdSpan) < 0); + Assert.True(emptyStringIdSpan.CompareTo(normalIdSpan) < 0); + } + } +}