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
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
}