Skip to content

Commit

Permalink
feat: support .NET 8.0 | Add new methods for IRandom (#387)
Browse files Browse the repository at this point in the history
  • Loading branch information
vbreuss committed Aug 24, 2023
1 parent 7f490bf commit b5644f6
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 2 deletions.
24 changes: 24 additions & 0 deletions Source/Testably.Abstractions.Interface/Helpers/RandomWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ public RandomWrapper(Random instance)

#region IRandom Members

#if FEATURE_RANDOM_ITEMS
/// <inheritdoc cref="IRandom.GetItems{T}(ReadOnlySpan{T}, Span{T})" />
public void GetItems<T>(ReadOnlySpan<T> choices, Span<T> destination)
=> _instance.GetItems(choices, destination);

/// <inheritdoc cref="IRandom.GetItems{T}(T[], int)" />
public T[] GetItems<T>(T[] choices, int length)
=> _instance.GetItems(choices, length);

/// <inheritdoc cref="IRandom.GetItems{T}(ReadOnlySpan{T}, int)" />
public T[] GetItems<T>(ReadOnlySpan<T> choices, int length)
=> _instance.GetItems(choices, length);
#endif

/// <inheritdoc cref="IRandom.Next()" />
public int Next()
=> _instance.Next();
Expand Down Expand Up @@ -65,5 +79,15 @@ public float NextSingle()
=> _instance.NextSingle();
#endif

#if FEATURE_RANDOM_ITEMS
/// <inheritdoc cref="IRandom.Shuffle{T}(T[])" />
public void Shuffle<T>(T[] values)
=> _instance.Shuffle(values);

/// <inheritdoc cref="IRandom.Shuffle{T}(Span{T})" />
public void Shuffle<T>(Span<T> values)
=> _instance.Shuffle(values);
#endif

#endregion
}
19 changes: 19 additions & 0 deletions Source/Testably.Abstractions.Interface/RandomSystem/IRandom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ namespace Testably.Abstractions.RandomSystem;
/// </summary>
public interface IRandom
{
#if FEATURE_RANDOM_ITEMS
/// <inheritdoc cref="Random.GetItems{T}(ReadOnlySpan{T}, Span{T})" />
void GetItems<T>(ReadOnlySpan<T> choices, Span<T> destination);

/// <inheritdoc cref="Random.GetItems{T}(T[], int)" />
T[] GetItems<T>(T[] choices, int length);

/// <inheritdoc cref="Random.GetItems{T}(ReadOnlySpan{T}, int)" />
T[] GetItems<T>(ReadOnlySpan<T> choices, int length);
#endif

/// <inheritdoc cref="Random.Next()" />
int Next();

Expand Down Expand Up @@ -40,4 +51,12 @@ public interface IRandom
/// <inheritdoc cref="Random.NextSingle()" />
float NextSingle();
#endif

#if FEATURE_RANDOM_ITEMS
/// <inheritdoc cref="Random.Shuffle{T}(T[])" />
void Shuffle<T>(T[] values);

/// <inheritdoc cref="Random.Shuffle{T}(Span{T})" />
void Shuffle<T>(Span<T> values);
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ internal static IOException SeekBackwardNotPossibleInAppendMode()
"Unable seek backward to overwrite data that previously existed in a file opened in Append mode.",
-2146232800);

internal static ArgumentException SpanMayNotBeEmpty(string paramName)
=> new("Span may not be empty.", paramName)
{
#if FEATURE_EXCEPTION_HRESULT
HResult = -2147024809
#endif
};

internal static NotSupportedException StreamDoesNotSupportReading()
=> new("Stream does not support reading.")
{
Expand Down
64 changes: 62 additions & 2 deletions Source/Testably.Abstractions.Testing/RandomSystem/RandomMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,39 @@ public RandomMock(

#region IRandom Members

#if FEATURE_RANDOM_ITEMS
/// <inheritdoc cref="IRandom.GetItems{T}(ReadOnlySpan{T}, Span{T})" />
public void GetItems<T>(ReadOnlySpan<T> choices, Span<T> destination)
{
if (choices.IsEmpty)
{
throw ExceptionFactory.SpanMayNotBeEmpty(nameof(choices));
}

for (int i = 0; i < destination.Length; i++)
{
destination[i] = choices[Next(choices.Length)];
}
}

/// <inheritdoc cref="IRandom.GetItems{T}(T[], int)" />
public T[] GetItems<T>(T[] choices, int length)
{
ArgumentNullException.ThrowIfNull(choices);
return GetItems(new ReadOnlySpan<T>(choices), length);
}

/// <inheritdoc cref="IRandom.GetItems{T}(ReadOnlySpan{T}, int)" />
public T[] GetItems<T>(ReadOnlySpan<T> choices, int length)
{
ArgumentOutOfRangeException.ThrowIfNegative(length);

T[] items = new T[length];
GetItems(choices, items.AsSpan());
return items;
}
#endif

/// <inheritdoc cref="IRandom.Next()" />
public int Next()
=> _intGenerator?.GetNext() ?? _random.Next();
Expand Down Expand Up @@ -115,8 +148,6 @@ public void NextBytes(Span<byte> buffer)
public double NextDouble()
=> _doubleGenerator?.GetNext() ?? _random.NextDouble();

#endregion

#if FEATURE_RANDOM_ADVANCED
private readonly Generator<long>? _longGenerator;
private readonly Generator<float>? _singleGenerator;
Expand Down Expand Up @@ -145,4 +176,33 @@ public long NextInt64(long minValue, long maxValue)
public float NextSingle()
=> _singleGenerator?.GetNext() ?? _random.NextSingle();
#endif

#if FEATURE_RANDOM_ITEMS
/// <inheritdoc cref="IRandom.Shuffle{T}(T[])" />
public void Shuffle<T>(T[] values)
{
ArgumentNullException.ThrowIfNull(values);
Shuffle(values.AsSpan());
}

/// <inheritdoc cref="IRandom.Shuffle{T}(Span{T})" />
public void Shuffle<T>(Span<T> values)
{
int n = values.Length;

for (int i = 0; i < n - 1; i++)
{
int j = Next(i, n);

if (j != i)
{
T temp = values[i];
values[i] = values[j];
values[j] = temp;
}
}
}
#endif

#endregion
}
164 changes: 164 additions & 0 deletions Tests/Testably.Abstractions.Tests/RandomSystem/RandomTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Collections.Concurrent;
using System.Threading.Tasks;
#if FEATURE_RANDOM_ITEMS
using System.Linq;
#endif

namespace Testably.Abstractions.Tests.RandomSystem;

Expand All @@ -8,6 +11,123 @@ public abstract partial class RandomTests<TRandomSystem>
: RandomSystemTestBase<TRandomSystem>
where TRandomSystem : IRandomSystem
{
#if FEATURE_RANDOM_ITEMS
[Fact]
public void GetItems_Array_EmptyChoices_ShouldThrowArgumentNullException()
{
int[] choices = Array.Empty<int>();

Exception? exception = Record.Exception(() =>
{
RandomSystem.Random.Shared.GetItems(choices, 1);
});

exception.Should().BeException<ArgumentException>("Span may not be empty",
hResult: -2147024809, paramName: nameof(choices));
}

[Fact]
public void GetItems_Array_NullChoices_ShouldThrowArgumentNullException()
{
int[] choices = null!;

Exception? exception = Record.Exception(() =>
{
RandomSystem.Random.Shared.GetItems(choices, -1);
});

exception.Should().BeOfType<ArgumentNullException>();
}

[Fact]
public void GetItems_Array_LengthLargerThanChoices_ShouldIncludeDuplicateValues()
{
int[] choices = Enumerable.Range(0, 10).ToArray();

int[] result = RandomSystem.Random.Shared.GetItems(choices, 100);

result.Length.Should().Be(100);
result.Should().OnlyContain(r => choices.Contains(r));
}

[Theory]
[InlineData(-1)]
[InlineData(-200)]
public void GetItems_Array_NegativeLength_ShouldThrowArgumentOutOfRangeException(int length)
{
int[] choices = Enumerable.Range(0, 10).ToArray();

Exception? exception = Record.Exception(() =>
{
RandomSystem.Random.Shared.GetItems(choices, length);
});

exception.Should().BeOfType<ArgumentOutOfRangeException>()
.Which.Message.Should()
.Be(
$"length ('{length}') must be a non-negative value. (Parameter 'length'){Environment.NewLine}Actual value was {length}.");
}

[Fact]
public void GetItems_Array_ShouldSelectRandomElements()
{
int[] choices = Enumerable.Range(0, 100).ToArray();

int[] result = RandomSystem.Random.Shared.GetItems(choices, 10);

result.Length.Should().Be(10);
result.Should().OnlyContain(r => choices.Contains(r));
}

[Fact]
public void GetItems_ReadOnlySpan_LengthLargerThanChoices_ShouldIncludeDuplicateValues()
{
ReadOnlySpan<int> choices = Enumerable.Range(0, 10).ToArray().AsSpan();

int[] result = RandomSystem.Random.Shared.GetItems(choices, 100);

result.Length.Should().Be(100);
result.Should().OnlyContain(r => r >= 0 && r < 10);
}

[Fact]
public void GetItems_ReadOnlySpan_ShouldSelectRandomElements()
{
ReadOnlySpan<int> choices = Enumerable.Range(0, 100).ToArray().AsSpan();

int[] result = RandomSystem.Random.Shared.GetItems(choices, 10);

result.Length.Should().Be(10);
result.Should().OnlyContain(r => r >= 0 && r < 100);
}

[Fact]
public void GetItems_SpanDestination_LengthLargerThanChoices_ShouldIncludeDuplicateValues()
{
int[] buffer = new int[100];
Span<int> destination = new(buffer);
ReadOnlySpan<int> choices = Enumerable.Range(0, 10).ToArray().AsSpan();

RandomSystem.Random.Shared.GetItems(choices, destination);

destination.Length.Should().Be(100);
destination.ToArray().Should().OnlyContain(r => r >= 0 && r < 10);
}

[Fact]
public void GetItems_SpanDestination_ShouldSelectRandomElements()
{
int[] buffer = new int[10];
Span<int> destination = new(buffer);
ReadOnlySpan<int> choices = Enumerable.Range(0, 100).ToArray().AsSpan();

RandomSystem.Random.Shared.GetItems(choices, destination);

destination.Length.Should().Be(10);
destination.ToArray().Should().OnlyContain(r => r >= 0 && r < 100);
}
#endif

[SkippableFact]
public void Next_MaxValue_ShouldOnlyReturnValidValues()
{
Expand Down Expand Up @@ -151,4 +271,48 @@ public void NextSingle_ShouldBeThreadSafe()
results.Should().OnlyHaveUniqueItems();
}
#endif

#if FEATURE_RANDOM_ITEMS
[Fact]
public void Shuffle_Array_ShouldShuffleItemsInPlace()
{
int[] originalValues = Enumerable.Range(0, 100).ToArray();
int[] values = originalValues.ToArray();

RandomSystem.Random.Shared.Shuffle(values);

values.Should().OnlyHaveUniqueItems();
values.Should().NotContainInOrder(originalValues);
values.OrderBy(x => x).Should().ContainInOrder(originalValues);
}

[Fact]
public void Shuffle_Array_Null_ShouldThrowArgumentNullException()
{
int[] values = null!;

Exception? exception = Record.Exception(() =>
{
RandomSystem.Random.Shared.Shuffle(values);
});

exception.Should().BeOfType<ArgumentNullException>()
.Which.ParamName.Should().Be(nameof(values));
}

[Fact]
public void Shuffle_Span_ShouldShuffleItemsInPlace()
{
int[] originalValues = Enumerable.Range(0, 100).ToArray();
int[] buffer = originalValues.ToArray();
Span<int> values = new(buffer);

RandomSystem.Random.Shared.Shuffle(values);

int[] result = values.ToArray();
result.Should().OnlyHaveUniqueItems();
result.Should().NotContainInOrder(originalValues);
result.OrderBy(x => x).Should().ContainInOrder(originalValues);
}
#endif
}

0 comments on commit b5644f6

Please sign in to comment.