diff --git a/README.md b/README.md
index c023896..eee4572 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# FEFF.TestFixtures
[](https://github.com/metacoder-feff/FEFF.TestFixtures/actions/workflows/test.yml)
-[](https://github.com/metacoder-feff/FEFF.TestFixtures/actions/workflows/package.yml)
+[](https://github.com/metacoder-feff/FEFF.TestFixtures/actions/workflows/build.yml)
Integrations:
diff --git a/src/FEFF.TestFixtures.AspNetCore/FakeRandom/FakeRandom.cs b/src/FEFF.TestFixtures.AspNetCore/FakeRandom/FakeRandom.cs
index af9e7d2..5704a65 100644
--- a/src/FEFF.TestFixtures.AspNetCore/FakeRandom/FakeRandom.cs
+++ b/src/FEFF.TestFixtures.AspNetCore/FakeRandom/FakeRandom.cs
@@ -1,7 +1,8 @@
namespace FEFF.TestFixtures.AspNetCore.Randomness;
///
-/// A configurable random number generator for testing.
+/// A configurable random number generator for testing.
+/// All public methods ARE threadsafe via lock().
///
///
/// The default behavior uses a constant seed. Additional strategies such as
@@ -40,6 +41,16 @@ namespace FEFF.TestFixtures.AspNetCore.Randomness;
///
public class FakeRandom : Random
{
+
+ #if NET9_0_OR_GREATER
+ private readonly Lock _lock = new();
+ #else
+ private readonly Object _lock = new();
+ #endif
+ private INormalizationStrategy _normalizationStrategy = new ThrowNormalization();
+
+ #region INextStrategy options
+
///
/// Gets or sets the strategy for generating values, or null to use the default.
///
@@ -64,8 +75,7 @@ public class FakeRandom : Random
/// Gets or sets the strategy for generating values, or null to use the default.
///
public INextStrategy? ByteNext { get; set; }
-
- private INormalizationStrategy _normalizationStrategy = new ThrowNormalization();
+ #endregion
///
/// Gets or sets the normalization strategy used when a generated value falls outside the valid
@@ -108,42 +118,51 @@ private int NextI32Internal(INextStrategy int32Next, int min, int max)
///
public override int Next()
{
- // just do as other "Assert arguments" pattern
- var def = base.Next();
+ lock(_lock)
+ {
+ // just do as other "Assert arguments" pattern
+ var def = base.Next();
- // avoid race condition
- var strategy = Int32Next;
+ // avoid race condition
+ var strategy = Int32Next;
- if (strategy == null)
- return def;
+ if (strategy == null)
+ return def;
- // int32Next is not null here
- return NextI32Internal(strategy, 0, int.MaxValue);
+ // int32Next is not null here
+ return NextI32Internal(strategy, 0, int.MaxValue);
+ }
}
///
public override int Next(int maxValue)
{
- // Assert arguments
- var def = base.Next(maxValue);
+ lock(_lock)
+ {
+ // Assert arguments
+ var def = base.Next(maxValue);
- var strategy = Int32Next;
- if (strategy == null)
- return def;
+ var strategy = Int32Next;
+ if (strategy == null)
+ return def;
- return NextI32Internal(strategy, 0, maxValue);
+ return NextI32Internal(strategy, 0, maxValue);
+ }
}
///
public override int Next(int minValue, int maxValue)
{
- // Assert arguments
- var def = base.Next(minValue, maxValue);
- var strategy = Int32Next;
- if (strategy == null)
- return def;
+ lock(_lock)
+ {
+ // Assert arguments
+ var def = base.Next(minValue, maxValue);
+ var strategy = Int32Next;
+ if (strategy == null)
+ return def;
- return NextI32Internal(strategy, minValue, maxValue);
+ return NextI32Internal(strategy, minValue, maxValue);
+ }
}
#endregion
@@ -166,36 +185,45 @@ private long NextI64Internal(INextStrategy int64Next, long min, long max)
///
public override long NextInt64()
{
- var def = base.NextInt64();
- var strategy = Int64Next;
- if (strategy == null)
- return def;
+ lock(_lock)
+ {
+ var def = base.NextInt64();
+ var strategy = Int64Next;
+ if (strategy == null)
+ return def;
- return NextI64Internal(strategy, 0, long.MaxValue);
+ return NextI64Internal(strategy, 0, long.MaxValue);
+ }
}
///
public override long NextInt64(long maxValue)
{
- // Assert arguments
- var def = base.NextInt64(maxValue);
- var strategy = Int64Next;
- if (strategy == null)
- return def;
+ lock(_lock)
+ {
+ // Assert arguments
+ var def = base.NextInt64(maxValue);
+ var strategy = Int64Next;
+ if (strategy == null)
+ return def;
- return NextI64Internal(strategy, 0, maxValue);
+ return NextI64Internal(strategy, 0, maxValue);
+ }
}
///
public override long NextInt64(long minValue, long maxValue)
{
- // Assert arguments
- var def = base.NextInt64(minValue, maxValue);
- var strategy = Int64Next;
- if (strategy == null)
- return def;
+ lock(_lock)
+ {
+ // Assert arguments
+ var def = base.NextInt64(minValue, maxValue);
+ var strategy = Int64Next;
+ if (strategy == null)
+ return def;
- return NextI64Internal(strategy, minValue, maxValue);
+ return NextI64Internal(strategy, minValue, maxValue);
+ }
}
#endregion
@@ -215,12 +243,15 @@ private float NextSingleInternal(INextStrategy singleNext)
///
public override float NextSingle()
{
- var def = base.NextSingle();
- var strategy = SingleNext;
- if (strategy == null)
- return def;
+ lock(_lock)
+ {
+ var def = base.NextSingle();
+ var strategy = SingleNext;
+ if (strategy == null)
+ return def;
- return NextSingleInternal(strategy);
+ return NextSingleInternal(strategy);
+ }
}
// doubleNext is not null here
@@ -237,12 +268,15 @@ private double NextDoubleInternal(INextStrategy doubleNext)
///
public override double NextDouble()
{
- var def = base.NextDouble();
- var strategy = DoubleNext;
- if (strategy == null)
- return def;
+ lock(_lock)
+ {
+ var def = base.NextDouble();
+ var strategy = DoubleNext;
+ if (strategy == null)
+ return def;
- return NextDoubleInternal(strategy);
+ return NextDoubleInternal(strategy);
+ }
}
#endregion
@@ -257,15 +291,18 @@ public override void NextBytes(byte[] buffer)
///
public override void NextBytes(Span buffer)
{
- // Assert arguments
- base.NextBytes(buffer);
-
- var strategy = ByteNext;
- if (strategy != null)
+ lock(_lock)
{
- for (int i = 0; i < buffer.Length; i++)
+ // Assert arguments
+ base.NextBytes(buffer);
+
+ var strategy = ByteNext;
+ if (strategy != null)
{
- buffer[i] = strategy.Next();
+ for (int i = 0; i < buffer.Length; i++)
+ {
+ buffer[i] = strategy.Next();
+ }
}
}
}
diff --git a/tests/FEFF.TestFixtures.Tests/Utils/FakeRandomTests/ThreadSafeTests.cs b/tests/FEFF.TestFixtures.Tests/Utils/FakeRandomTests/ThreadSafeTests.cs
new file mode 100644
index 0000000..f511875
--- /dev/null
+++ b/tests/FEFF.TestFixtures.Tests/Utils/FakeRandomTests/ThreadSafeTests.cs
@@ -0,0 +1,270 @@
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+
+namespace FEFF.TestFixtures.AspNetCore.Randomness.Tests;
+
+public class ThreadSafeTests
+{
+ private class RaceStrategy(T a, T b) : INextStrategy
+ {
+ private volatile int _counter = 0;
+ public T Next()
+ {
+ var current = Interlocked.Increment(ref _counter);
+ if(current == 1)
+ {
+ Thread.Sleep(1000);
+ return a;
+ }
+ return b;
+ }
+ }
+
+ // fast set by index by counter
+ private class ConcurrentList(int capacity)
+ {
+ private readonly T[] _list = new T[capacity];
+ private volatile int _nextIdx = -1;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Add(T item)
+ {
+ var current = Interlocked.Increment(ref _nextIdx);
+
+ if(current >= capacity)
+ throw new InvalidOperationException("List is full");
+
+ _list[current] = item;
+ }
+
+ internal List ToList()
+ {
+ var n = _nextIdx;
+ if(n < 0)
+ return [];
+ return _list.Take(n + 1).ToList();
+ }
+ }
+
+ private static RaceStrategy CreateRaceStrategyFrom(T a, T b) => new(a,b);
+
+ private const int ThreadCount = 2;
+ private readonly Barrier _barrier = new(ThreadCount);
+
+ protected FakeRandom Rand { get; } = new();
+
+ #region int-32
+
+ [Fact]
+ public void Next__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.Int32Next = CreateRaceStrategyFrom(1, 2);
+ var results = new ConcurrentQueue();
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.Next();
+ results.Enqueue(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 1, 2 ],
+ options => options.WithStrictOrdering()
+ );
+
+ // without lock it would be: [2, 1]
+ // because '1' has a delay
+ }
+
+ [Fact]
+ public void Next_max__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.Int32Next = CreateRaceStrategyFrom(10, 20);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.Next(100);
+ results.Add(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 10, 20 ],
+ options => options.WithStrictOrdering()
+ );
+ }
+
+ [Fact]
+ public void Next_min_max__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.Int32Next = CreateRaceStrategyFrom(100, 200);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.Next(50, 300);
+ results.Add(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 100, 200 ],
+ options => options.WithStrictOrdering()
+ );
+ }
+ #endregion
+
+ #region int-64
+
+ [Fact]
+ public void Next64__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.Int64Next = CreateRaceStrategyFrom(1L, 2L);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.NextInt64();
+ results.Add(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 1L, 2L ],
+ options => options.WithStrictOrdering()
+ );
+ }
+
+ [Fact]
+ public void Next64_max__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.Int64Next = CreateRaceStrategyFrom(10L, 20L);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.NextInt64(100L);
+ results.Add(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 10L, 20L ],
+ options => options.WithStrictOrdering()
+ );
+ }
+
+ [Fact]
+ public void Next64_min_max__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.Int64Next = CreateRaceStrategyFrom(100L, 200L);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.NextInt64(50L, 300L);
+ results.Add(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 100L, 200L ],
+ options => options.WithStrictOrdering()
+ );
+ }
+ #endregion
+
+ #region float/double
+
+ [Fact]
+ public void NextSingle__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.SingleNext = CreateRaceStrategyFrom(0.1f, 0.2f);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.NextSingle();
+ results.Add(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 0.1f, 0.2f ],
+ options => options.WithStrictOrdering()
+ );
+ }
+
+ [Fact]
+ public void NextDouble__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.DoubleNext = CreateRaceStrategyFrom(0.1, 0.2);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ _barrier.SignalAndWait();
+ var value = Rand.NextDouble();
+ results.Add(value);
+ });
+
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ 0.1, 0.2 ],
+ options => options.WithStrictOrdering()
+ );
+ }
+ #endregion
+
+ #region byte[]
+
+ [Fact]
+ public void NextBytes__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.ByteNext = CreateRaceStrategyFrom((byte)1, (byte)2);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ var buffer = new byte[1];
+ _barrier.SignalAndWait();
+ Rand.NextBytes(buffer);
+ results.Add(buffer[0]);
+ });
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ (byte)1, (byte)2 ],
+ options => options.WithStrictOrdering()
+ );
+ }
+
+ [Fact]
+ public void NextBytes_span__when_multi_threaded__should_not_mix_values()
+ {
+ Rand.ByteNext = CreateRaceStrategyFrom((byte)1, (byte)2);
+ var results = new ConcurrentList(ThreadCount);
+
+ Parallel.For(0, ThreadCount, _ =>
+ {
+ Span buffer = new byte[1];
+ _barrier.SignalAndWait();
+ Rand.NextBytes(buffer);
+ results.Add(buffer[0]);
+ });
+ results.ToList()
+ .Should().BeEquivalentTo(
+ [ (byte)1, (byte)2 ],
+ options => options.WithStrictOrdering()
+ );
+ }
+ #endregion
+}
\ No newline at end of file