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.