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
4 changes: 0 additions & 4 deletions etc/junit4-missing-features.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <code>owner</code> thread and delegating to <code>delegate
* </code> 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.");
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Random> {}
Original file line number Diff line number Diff line change
@@ -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<Random> seedToRandomFn;
private final RandomFactory randomFactory;

RandomizedContext(
String contextId,
RandomizedContext parent,
Thread owner,
LongFunction<Random> 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() {
Expand All @@ -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();
Expand All @@ -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();
}
}
}
Loading
Loading