diff --git a/etc/junit4-missing-features.txt b/etc/junit4-missing-features.txt
index 8efde54..65f5d04 100644
--- a/etc/junit4-missing-features.txt
+++ b/etc/junit4-missing-features.txt
@@ -1,9 +1,5 @@
[ai generated overview of junit4 features]
-*. injecting Random into test methods (based on the current context). Use non-synchronized
-Random implementation. Optionally (parameter?) enable verifying that this Random is not shared
-with other threads for reproducibility.
-
4. Shuffled test execution order and seed annotations
- @Seed on a class fixes the main seed, making execution fully deterministic
- @Seeds / @Seed on a method pins a per-method seed for regression coverage
diff --git a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/AssertingRandom.java b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/AssertingRandom.java
new file mode 100644
index 0000000..cf8f35d
--- /dev/null
+++ b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/AssertingRandom.java
@@ -0,0 +1,168 @@
+package com.carrotsearch.randomizedtesting.jupiter;
+
+import java.io.Closeable;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Random;
+
+/**
+ * A {@link Random} with a delegate, preventing {@link Random#setSeed(long)} and locked to only be
+ * usable by a single {@link Thread}.
+ */
+final class AssertingRandom extends Random implements Closeable {
+ private final Random delegate;
+ private final Thread ownerRef;
+ private final String ownerName;
+ private final StackTraceElement[] allocationStack;
+
+ /**
+ * Track out-of-context use of this {@link Random} instance. This introduces memory barriers and
+ * scheduling side effects but there's no other way to do it in any other way and sharing randoms
+ * across threads or test cases is very bad and worth tracking.
+ */
+ private volatile boolean valid = true;
+
+ /**
+ * Creates an instance to be used by owner thread and delegating to delegate
+ * until {@link #close()}ed.
+ */
+ public AssertingRandom(Thread owner, Random delegate) {
+ // Must be here, the only Random constructor. Has side effects on setSeed, see below.
+ super(0);
+
+ this.delegate = delegate;
+ this.ownerRef = Objects.requireNonNull(owner);
+ this.ownerName = owner.toString();
+ this.allocationStack = Thread.currentThread().getStackTrace();
+ }
+
+ @Override
+ protected int next(int bits) {
+ throw new RuntimeException("Shouldn't be reachable.");
+ }
+
+ @Override
+ public boolean nextBoolean() {
+ checkValid();
+ return delegate.nextBoolean();
+ }
+
+ @Override
+ public void nextBytes(byte[] bytes) {
+ checkValid();
+ delegate.nextBytes(bytes);
+ }
+
+ @Override
+ public double nextDouble() {
+ checkValid();
+ return delegate.nextDouble();
+ }
+
+ @Override
+ public float nextFloat() {
+ checkValid();
+ return delegate.nextFloat();
+ }
+
+ @Override
+ public double nextGaussian() {
+ checkValid();
+ return delegate.nextGaussian();
+ }
+
+ @Override
+ public int nextInt() {
+ checkValid();
+ return delegate.nextInt();
+ }
+
+ @Override
+ public int nextInt(int n) {
+ checkValid();
+ return delegate.nextInt(n);
+ }
+
+ @Override
+ public long nextLong() {
+ checkValid();
+ return delegate.nextLong();
+ }
+
+ @Override
+ public void setSeed(long seed) {
+ // This is an interesting case of observing uninitialized object from an instance method
+ // (this method is called from the superclass constructor).
+ if (seed == 0 && delegate == null) {
+ return;
+ }
+
+ throw noSetSeed();
+ }
+
+ @Override
+ public String toString() {
+ checkValid();
+ return delegate.toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ checkValid();
+ return delegate.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ checkValid();
+ return delegate.hashCode();
+ }
+
+ /** This object will no longer be usable after this method is called. */
+ public void close() {
+ this.valid = false;
+ }
+
+ private static final class StackTraceHolder extends Throwable {
+ public StackTraceHolder(String message) {
+ super(message);
+ }
+ }
+
+ /* */
+ private void checkValid() {
+ if (!valid) {
+ throw new RuntimeException(
+ "This Random instance has been invalidated and "
+ + "is probably used out of its allowed context (test or suite).");
+ }
+
+ if (Thread.currentThread() != ownerRef) {
+ Throwable allocationEx =
+ new StackTraceHolder(
+ "Original allocation stack for this Random (" + "allocated by " + ownerName + ")");
+ allocationEx.setStackTrace(allocationStack);
+ throw new RuntimeException(
+ String.format(
+ Locale.ROOT,
+ "This Random instance is tied to thread %s, can't access it from thread: %s "
+ + "(Random instances must not be shared). Allocation stack is included as a nested exception.",
+ ownerName,
+ Thread.currentThread()),
+ allocationEx);
+ }
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ checkValid();
+ throw new CloneNotSupportedException("Don't clone test Randoms.");
+ }
+
+ static RuntimeException noSetSeed() {
+ return new RuntimeException(
+ "Changing the seed of Random instances is forbidden, it breaks repeatability"
+ + " of tests. If you need a mutable instance of Random, create a new (local) instance,"
+ + " preferably with the initial seed acquired from this Random instance.");
+ }
+}
diff --git a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Hashing.java b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Hashing.java
index 423bd4c..744c1eb 100644
--- a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Hashing.java
+++ b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Hashing.java
@@ -1,6 +1,8 @@
package com.carrotsearch.randomizedtesting.jupiter;
-public class Hashing {
+/** Static hashing utilities. */
+public final class Hashing {
+ /** Bit mixer for {@code long} values. */
public static long mix64(long k) {
k ^= k >>> 33;
k *= 0xff51afd7ed558ccdL;
@@ -10,7 +12,7 @@ public static long mix64(long k) {
return k;
}
- /** String hash function redistributing over a long range. */
+ /** String hash function redistributing over a {@code long}. */
public static long longHash(String v) {
long h = 0;
int length = v.length();
diff --git a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomFactory.java b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomFactory.java
new file mode 100644
index 0000000..0946e12
--- /dev/null
+++ b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomFactory.java
@@ -0,0 +1,7 @@
+package com.carrotsearch.randomizedtesting.jupiter;
+
+import java.util.Random;
+import java.util.function.LongFunction;
+
+/** Supplier of {@link Random} instances, given the initial seed value. */
+public interface RandomFactory extends LongFunction {}
diff --git a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContext.java b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContext.java
index d653cb0..e9c20ee 100644
--- a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContext.java
+++ b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContext.java
@@ -1,48 +1,42 @@
package com.carrotsearch.randomizedtesting.jupiter;
+import java.io.Closeable;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Locale;
import java.util.Objects;
import java.util.Random;
-import java.util.function.LongFunction;
import org.junit.jupiter.api.extension.ExtensionContext;
-public final class RandomizedContext {
+public final class RandomizedContext implements Closeable {
private final RandomizedContext parent;
- private final Thread owner;
private final Seed seed;
final String contextId;
private final SeedChain remainingSeedChain;
private final Random random;
- private final LongFunction seedToRandomFn;
+ private final RandomFactory randomFactory;
RandomizedContext(
String contextId,
RandomizedContext parent,
- Thread owner,
- LongFunction seedToRandomFn,
+ RandomFactory randomFactory,
Seed seed,
SeedChain remainingSeedChain) {
this.contextId = contextId;
this.parent = parent;
- this.owner = owner;
this.remainingSeedChain = remainingSeedChain;
- this.seedToRandomFn = seedToRandomFn;
+ this.randomFactory = randomFactory;
assert !seed.isUnspecified();
this.seed = seed;
- this.random = seedToRandomFn.apply(seed.value());
+ this.random = randomFactory.apply(seed.value());
}
@Override
public String toString() {
- return "Randomized context ["
- + ("seedChain=" + getSeedChain() + ",")
- + ("thread=" + Threads.threadName(owner))
- + "]";
+ return "Randomized context [" + ("seedChain=" + getSeedChain() + ",") + "]";
}
SeedChain getSeedChain() {
@@ -67,20 +61,10 @@ private RandomizedContext getParent() {
}
public Random getRandom() {
- if (Thread.currentThread() != owner) {
- throw new RuntimeException(
- String.format(
- Locale.ROOT,
- "This %s instance is bound to thread %s, can't access it from thread: %s",
- RandomizedContext.class.getName(),
- owner,
- Thread.currentThread()));
- }
-
return random;
}
- RandomizedContext deriveNew(Thread thread, ExtensionContext extensionContext) {
+ RandomizedContext deriveNew(ExtensionContext extensionContext) {
// sanity check.
{
var id = extensionContext.getUniqueId();
@@ -99,11 +83,13 @@ RandomizedContext deriveNew(Thread thread, ExtensionContext extensionContext) {
}
return new RandomizedContext(
- extensionContext.getUniqueId(),
- this,
- thread,
- seedToRandomFn,
- nextSeed,
- firstAndRest.rest());
+ extensionContext.getUniqueId(), this, randomFactory, nextSeed, firstAndRest.rest());
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (random instanceof Closeable c) {
+ c.close();
+ }
}
}
diff --git a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContextSupplier.java b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContextSupplier.java
index 63f7a98..e209093 100644
--- a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContextSupplier.java
+++ b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContextSupplier.java
@@ -3,9 +3,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler;
@@ -26,7 +30,23 @@ public class RandomizedContextSupplier
/** System properties controlling the extension. */
public enum SysProps {
- TESTS_SEED("tests.seed");
+ /** Initial root seed value. If empty, a random value is picked for the root seed. */
+ TESTS_SEED("tests.seed"),
+
+ /**
+ * String name of the factory used to create {@link Random} instances (see {@link
+ * RandomFactoryType} for named implementations).
+ *
+ * @see RandomFactoryType
+ */
+ TESTS_RANDOM_FACTORY("tests.random.factory"),
+
+ /**
+ * A boolean property that enables stricter sanity assertions (including forbidding
+ * thread-shared access to the returned {@link Random} instances, which makes tests more
+ * predictable).
+ */
+ TESTS_RANDOM_ASSERTING("tests.random.asserting");
public final String propertyKey;
@@ -35,12 +55,45 @@ public enum SysProps {
}
}
+ public enum RandomFactoryType implements Supplier {
+ JDK,
+ XOROSHIRO_128_PLUS;
+
+ public static RandomFactoryType parse(String v) {
+ try {
+ return RandomFactoryType.valueOf(v.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Can't parse "
+ + SysProps.TESTS_RANDOM_FACTORY.propertyKey
+ + " property: "
+ + v
+ + " [valid values: "
+ + Stream.of(RandomFactoryType.values())
+ .map(vv -> vv.name().toLowerCase(Locale.ROOT))
+ .collect(Collectors.joining(", "))
+ + "]");
+ }
+ }
+
+ @Override
+ public RandomFactory get() {
+ return switch (this) {
+ case JDK -> Random::new;
+ case XOROSHIRO_128_PLUS -> Xoroshiro128PlusRandom::new;
+ };
+ }
+ }
+
//
// before-all (class-level context setup).
//
@Override
public void beforeAll(ExtensionContext extensionContext) {
+ // Set up Random factory.
+ RandomFactory randomFactory = initializeRandomFactory(extensionContext);
+
// Bootstrap the root store's context. Don't know if this can be done
// in a more elegant way.
extensionContext
@@ -59,13 +112,32 @@ public void beforeAll(ExtensionContext extensionContext) {
return new RandomizedContext(
extensionContext.getRoot().getUniqueId(),
null,
- Thread.currentThread(),
- Random::new,
+ randomFactory,
firstAndRest.first(),
firstAndRest.rest());
});
}
+ private static RandomFactory initializeRandomFactory(ExtensionContext extensionContext) {
+ RandomFactory randomFactory =
+ extensionContext
+ .getConfigurationParameter(SysProps.TESTS_RANDOM_FACTORY.propertyKey)
+ .map(RandomFactoryType::parse)
+ .orElse(RandomFactoryType.XOROSHIRO_128_PLUS)
+ .get();
+
+ if (extensionContext
+ .getConfigurationParameter(SysProps.TESTS_RANDOM_ASSERTING.propertyKey)
+ .map(Boolean::parseBoolean)
+ .orElse(RandomizedContextSupplier.class.desiredAssertionStatus())) {
+ var delegateFactory = randomFactory;
+ randomFactory =
+ seed -> new AssertingRandom(Thread.currentThread(), delegateFactory.apply(seed));
+ }
+
+ return randomFactory;
+ }
+
/**
* @return Returns the constant root seed, initialized from an optional configuration parameter.
*/
@@ -82,21 +154,31 @@ private static SeedChain parseRootSeed(Optional rootSeedValue) {
}
//
- // ParameterResolver: inject RandomizedContext into test methods.
+ // ParameterResolver: inject RandomizedContext and Random instances into test methods.
//
@Override
public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
- return parameterContext.getParameter().getType().equals(RandomizedContext.class);
+ Class> parameterType = parameterContext.getParameter().getType();
+ return parameterType.equals(RandomizedContext.class) || parameterType.equals(Random.class);
}
@Override
- public RandomizedContext resolveParameter(
+ public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
- return getRandomizedContextFor(extensionContext);
+ var ctx = getRandomizedContextFor(extensionContext);
+ Class> parameterType = parameterContext.getParameter().getType();
+ if (parameterType.equals(RandomizedContext.class)) {
+ return ctx;
+ } else if (parameterType.equals(Random.class)) {
+ return ctx.getRandom();
+ } else {
+ throw new RuntimeException(
+ "Unexpected unsupported parameter type in resolveParameter: " + parameterType);
+ }
}
//
@@ -116,7 +198,7 @@ private RandomizedContext getRandomizedContextFor(ExtensionContext extensionCont
// No context for this context yet.
var parentContext = getRandomizedContextFor(extensionContext.getParent().orElseThrow());
- thisContext = parentContext.deriveNew(Thread.currentThread(), extensionContext);
+ thisContext = parentContext.deriveNew(extensionContext);
store.put(CTX_KEY_RANDOMIZED_CONTEXT, thisContext);
return thisContext;
}
diff --git a/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Xoroshiro128PlusRandom.java b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Xoroshiro128PlusRandom.java
new file mode 100644
index 0000000..829219e
--- /dev/null
+++ b/randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Xoroshiro128PlusRandom.java
@@ -0,0 +1,96 @@
+package com.carrotsearch.randomizedtesting.jupiter;
+
+import java.util.Random;
+
+/**
+ * Implements Xoroshiro128PlusRandom. Not synchronized (anywhere).
+ *
+ * @see "https://prng.di.unimi.it/"
+ */
+final class Xoroshiro128PlusRandom extends Random {
+ private static final double DOUBLE_UNIT = 0x1.0p-53; // 1.0 / (1L << 53);
+ private static final float FLOAT_UNIT = 0x1.0p-24f; // 1.0 / (1L << 24);
+
+ private long s0, s1;
+
+ public Xoroshiro128PlusRandom(long seed) {
+ // Must be here, the only Random constructor. Has side-effects on setSeed, see below.
+ super(0);
+
+ s0 = Hashing.mix64(seed);
+ s1 = Hashing.mix64(s0);
+
+ if (s0 == 0 && s1 == 0) {
+ s0 = Hashing.mix64(0xdeadbeefL);
+ s1 = Hashing.mix64(s0);
+ }
+ }
+
+ @Override
+ public void setSeed(long seed) {
+ // Called from super constructor and observing uninitialized state?
+ if (s0 == 0 && s1 == 0) {
+ return;
+ }
+
+ throw AssertingRandom.noSetSeed();
+ }
+
+ @Override
+ public boolean nextBoolean() {
+ return nextLong() >= 0;
+ }
+
+ @Override
+ public void nextBytes(byte[] bytes) {
+ for (int i = 0, len = bytes.length; i < len; ) {
+ long rnd = nextInt();
+ for (int n = Math.min(len - i, 8); n-- > 0; rnd >>>= 8) {
+ bytes[i++] = (byte) rnd;
+ }
+ }
+ }
+
+ @Override
+ public double nextDouble() {
+ return (nextLong() >>> 11) * DOUBLE_UNIT;
+ }
+
+ @Override
+ public float nextFloat() {
+ return (nextInt() >>> 8) * FLOAT_UNIT;
+ }
+
+ @Override
+ public int nextInt() {
+ return (int) nextLong();
+ }
+
+ @Override
+ public int nextInt(int n) {
+ // Leave superclass's implementation.
+ return super.nextInt(n);
+ }
+
+ @Override
+ public double nextGaussian() {
+ // Leave superclass's implementation.
+ return super.nextGaussian();
+ }
+
+ @Override
+ public long nextLong() {
+ final long s0 = this.s0;
+ long s1 = this.s1;
+ final long result = s0 + s1;
+ s1 ^= s0;
+ this.s0 = Long.rotateLeft(s0, 55) ^ s1 ^ s1 << 14;
+ this.s1 = Long.rotateLeft(s1, 36);
+ return result;
+ }
+
+ @Override
+ protected int next(int bits) {
+ return ((int) nextLong()) >>> (32 - bits);
+ }
+}
diff --git a/randomizedtesting-jupiter/src/test/java/com/carrotsearch/randomizedtesting/jupiter/F003_RandomInjection.java b/randomizedtesting-jupiter/src/test/java/com/carrotsearch/randomizedtesting/jupiter/F003_RandomInjection.java
new file mode 100644
index 0000000..5411f52
--- /dev/null
+++ b/randomizedtesting-jupiter/src/test/java/com/carrotsearch/randomizedtesting/jupiter/F003_RandomInjection.java
@@ -0,0 +1,214 @@
+package com.carrotsearch.randomizedtesting.jupiter;
+
+import static com.carrotsearch.randomizedtesting.jupiter.infra.TestInfra.*;
+import static org.junit.platform.testkit.engine.EventConditions.*;
+
+import com.carrotsearch.randomizedtesting.jupiter.infra.IgnoreInStandaloneRuns;
+import java.io.PrintWriter;
+import java.util.Locale;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.api.TestInstance;
+
+/** Verifies that {@link java.util.Random} instances are properly injected as parameters. */
+public class F003_RandomInjection {
+ @Nested
+ class TestRandomInjection {
+ @Test
+ public void testAllHooks() {
+ collectExecutionResults(testKitBuilder(T1.class))
+ .results()
+ .allEvents()
+ .assertThatEvents()
+ .doNotHave(event(finishedWithFailure()));
+ }
+
+ @Randomized
+ static class T1 extends IgnoreInStandaloneRuns {
+ public T1(Random random) {
+ Assertions.assertThat(random).isNotNull();
+ }
+
+ @BeforeAll
+ static void beforeAll(Random random) {
+ Assertions.assertThat(random).isNotNull();
+ }
+
+ @BeforeEach
+ void beforeEach(Random random) {
+ Assertions.assertThat(random).isNotNull();
+ }
+
+ @Test
+ void testMethod(Random random) {
+ Assertions.assertThat(random).isNotNull();
+ }
+
+ @AfterEach
+ void afterEach(Random random) {
+ Assertions.assertThat(random).isNotNull();
+ }
+
+ @AfterAll
+ static void afterAll(Random random) {
+ Assertions.assertThat(random).isNotNull();
+ }
+ }
+ }
+
+ @Nested
+ class TestRandomFactoryAndState {
+ @Test
+ public void randomInitializedWithContextSeed() {
+ var executionResults =
+ IntStream.range(0, 5)
+ .mapToObj(
+ unused ->
+ collectExecutionResults(
+ testKitBuilder(T1.class)
+ .configurationParameter(
+ RandomizedContextSupplier.SysProps.TESTS_SEED.propertyKey,
+ "deadbeef")))
+ .toList();
+
+ Assertions.assertThat(
+ executionResults.stream()
+ .flatMap(r -> r.capturedOutput().values().stream())
+ .collect(Collectors.toSet()))
+ .hasSize(1);
+ }
+
+ @TestFactory
+ public Stream checkAllRandomFactories() {
+ return Stream.of(RandomizedContextSupplier.RandomFactoryType.values())
+ .map(
+ t -> {
+ return DynamicTest.dynamicTest(
+ t.name(),
+ () -> {
+ var expectedClass = t.get().apply(0).getClass().getName();
+
+ var executionResult =
+ collectExecutionResults(
+ testKitBuilder(T1.class)
+ .configurationParameter(
+ RandomizedContextSupplier.SysProps.TESTS_RANDOM_FACTORY
+ .propertyKey,
+ t.name().toLowerCase(Locale.ROOT))
+ .configurationParameter(
+ RandomizedContextSupplier.SysProps.TESTS_RANDOM_ASSERTING
+ .propertyKey,
+ "false"));
+ executionResult
+ .results()
+ .allEvents()
+ .assertThatEvents()
+ .doNotHave(event(finishedWithFailure()));
+
+ Assertions.assertThat(executionResult.capturedOutput().values())
+ .containsOnly(expectedClass);
+ });
+ });
+ }
+
+ @Randomized
+ static class T1 extends IgnoreInStandaloneRuns {
+ @Test
+ void testMethod(PrintWriter pw, Random rnd) {
+ pw.print(rnd.getClass().getName());
+ }
+ }
+ }
+
+ @Nested
+ class TestRandomAssertions {
+ @Test
+ public void testAllHooks() {
+ collectExecutionResults(
+ testKitBuilder(T1.class)
+ .configurationParameter(
+ RandomizedContextSupplier.SysProps.TESTS_RANDOM_ASSERTING.propertyKey,
+ "true"))
+ .results()
+ .allEvents()
+ .assertThatEvents()
+ .doNotHave(event(finishedWithFailure()));
+ }
+
+ @Randomized
+ static class T1 extends IgnoreInStandaloneRuns {
+ @Test
+ void testMethod(Random random) throws Exception {
+ var ex = new AtomicReference();
+ var thread =
+ new Thread(
+ () -> {
+ try {
+ random.nextLong();
+ } catch (Exception e) {
+ ex.set(e);
+ }
+ });
+ thread.start();
+ thread.join();
+
+ Assertions.assertThat(ex.get())
+ .isNotNull()
+ .isExactlyInstanceOf(RuntimeException.class)
+ .hasMessageContaining("This Random instance is tied to thread");
+ }
+ }
+
+ @Test
+ public void testAssertingRandomIsClosedAfterContext() {
+ collectExecutionResults(
+ testKitBuilder(T2.class)
+ .configurationParameter(
+ RandomizedContextSupplier.SysProps.TESTS_RANDOM_ASSERTING.propertyKey,
+ "true"))
+ .results()
+ .allEvents()
+ .assertThatEvents()
+ .doNotHave(event(finishedWithFailure()));
+ }
+
+ @Randomized
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ static class T2 extends IgnoreInStandaloneRuns {
+ Random rnd;
+
+ @Test
+ void testMethod1(Random random) throws Exception {
+ check(random);
+ }
+
+ @Test
+ void testMethod2(Random random) throws Exception {
+ check(random);
+ }
+
+ private void check(Random random) {
+ if (rnd == null) {
+ rnd = random;
+ } else {
+ Assertions.assertThat(rnd).isNotSameAs(random);
+ Assertions.assertThatCode(() -> rnd.nextLong())
+ .isExactlyInstanceOf(RuntimeException.class)
+ .hasMessageContaining("This Random instance has been invalidated");
+ }
+ }
+ }
+ }
+}
diff --git a/randomizedtesting-jupiter/src/test/java/com/carrotsearch/randomizedtesting/jupiter/F003_RandomInjection.md b/randomizedtesting-jupiter/src/test/java/com/carrotsearch/randomizedtesting/jupiter/F003_RandomInjection.md
new file mode 100644
index 0000000..6c7beea
--- /dev/null
+++ b/randomizedtesting-jupiter/src/test/java/com/carrotsearch/randomizedtesting/jupiter/F003_RandomInjection.md
@@ -0,0 +1,32 @@
+# Feature: direct injection of Random instances
+
+## Functionality
+
+* It should be possible to get access to a `Random` object via parameters
+ injected into test methods and hooks. For example,
+
+```java
+
+@Randomized
+public class TestClass {
+ @Test
+ public void testMethod(Random ctx) {
+ }
+}
+```
+
+The injected `Random` is initialized with the context's seed.
+
+* It should be possible to pick (via system properties or JUnit5 configuratoin
+ parameters) different `Random` implementations for the
+ injected parameter. Lockless or those providing larger state space than
+ the default `java.util.Random`.
+
+* The injected `Random` is tied to the thread that created it. When
+ assertions are enabled (or an explicit parameter is set), the injected
+ Random instances should verify they are indeed used from within the
+ right thread.
+
+## Migration notes (from randomizedtesting for junit4)
+
+This is new functionality, it wasn't available before.