Skip to content

Commit 230fe25

Browse files
committed
Add direct Random parameter injection. Add AssertingRandom and synchronization-less Random factories.
1 parent 6f17547 commit 230fe25

9 files changed

Lines changed: 562 additions & 44 deletions

File tree

etc/junit4-missing-features.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
[ai generated overview of junit4 features]
22

3-
*. injecting Random into test methods (based on the current context). Use non-synchronized
4-
Random implementation. Optionally (parameter?) enable verifying that this Random is not shared
5-
with other threads for reproducibility.
6-
73
4. Shuffled test execution order and seed annotations
84
- @Seed on a class fixes the main seed, making execution fully deterministic
95
- @Seeds / @Seed on a method pins a per-method seed for regression coverage
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package com.carrotsearch.randomizedtesting.jupiter;
2+
3+
import java.util.Locale;
4+
import java.util.Objects;
5+
import java.util.Random;
6+
7+
/**
8+
* A {@link Random} with a delegate, preventing {@link Random#setSeed(long)} and locked to only be
9+
* usable by a single {@link Thread}.
10+
*/
11+
final class AssertingRandom extends Random {
12+
private final Random delegate;
13+
private final Thread ownerRef;
14+
private final String ownerName;
15+
private final StackTraceElement[] allocationStack;
16+
17+
/**
18+
* Track out-of-context use of this {@link Random} instance. This introduces memory barriers and
19+
* scheduling side effects but there's no other way to do it in any other way and sharing randoms
20+
* across threads or test cases is very bad and worth tracking.
21+
*/
22+
private volatile boolean valid = true;
23+
24+
/**
25+
* Creates an instance to be used by <code>owner</code> thread and delegating to <code>delegate
26+
* </code> until {@link #destroy()}ed.
27+
*/
28+
public AssertingRandom(Thread owner, Random delegate) {
29+
// Must be here, the only Random constructor. Has side effects on setSeed, see below.
30+
super(0);
31+
32+
this.delegate = delegate;
33+
this.ownerRef = Objects.requireNonNull(owner);
34+
this.ownerName = owner.toString();
35+
this.allocationStack = Thread.currentThread().getStackTrace();
36+
}
37+
38+
@Override
39+
protected int next(int bits) {
40+
throw new RuntimeException("Shouldn't be reachable.");
41+
}
42+
43+
@Override
44+
public boolean nextBoolean() {
45+
checkValid();
46+
return delegate.nextBoolean();
47+
}
48+
49+
@Override
50+
public void nextBytes(byte[] bytes) {
51+
checkValid();
52+
delegate.nextBytes(bytes);
53+
}
54+
55+
@Override
56+
public double nextDouble() {
57+
checkValid();
58+
return delegate.nextDouble();
59+
}
60+
61+
@Override
62+
public float nextFloat() {
63+
checkValid();
64+
return delegate.nextFloat();
65+
}
66+
67+
@Override
68+
public double nextGaussian() {
69+
checkValid();
70+
return delegate.nextGaussian();
71+
}
72+
73+
@Override
74+
public int nextInt() {
75+
checkValid();
76+
return delegate.nextInt();
77+
}
78+
79+
@Override
80+
public int nextInt(int n) {
81+
checkValid();
82+
return delegate.nextInt(n);
83+
}
84+
85+
@Override
86+
public long nextLong() {
87+
checkValid();
88+
return delegate.nextLong();
89+
}
90+
91+
@Override
92+
public void setSeed(long seed) {
93+
// This is an interesting case of observing uninitialized object from an instance method
94+
// (this method is called from the superclass constructor).
95+
if (seed == 0 && delegate == null) {
96+
return;
97+
}
98+
99+
throw noSetSeed();
100+
}
101+
102+
@Override
103+
public String toString() {
104+
checkValid();
105+
return delegate.toString();
106+
}
107+
108+
@Override
109+
public boolean equals(Object obj) {
110+
checkValid();
111+
return delegate.equals(obj);
112+
}
113+
114+
@Override
115+
public int hashCode() {
116+
checkValid();
117+
return delegate.hashCode();
118+
}
119+
120+
/** This object will no longer be usable after this method is called. */
121+
void destroy() {
122+
this.valid = false;
123+
}
124+
125+
private static final class StackTraceHolder extends Throwable {
126+
public StackTraceHolder(String message) {
127+
super(message);
128+
}
129+
}
130+
131+
/* */
132+
private void checkValid() {
133+
if (!valid) {
134+
throw new RuntimeException(
135+
"This Random instance has been invalidated and "
136+
+ "is probably used out of its allowed context (test or suite).");
137+
}
138+
139+
if (Thread.currentThread() != ownerRef) {
140+
Throwable allocationEx =
141+
new StackTraceHolder(
142+
"Original allocation stack for this Random (" + "allocated by " + ownerName + ")");
143+
allocationEx.setStackTrace(allocationStack);
144+
throw new RuntimeException(
145+
String.format(
146+
Locale.ROOT,
147+
"This Random instance is tied to thread %s, can't access it from thread: %s "
148+
+ "(Random instances must not be shared). Allocation stack is included as a nested exception.",
149+
ownerName,
150+
Thread.currentThread()),
151+
allocationEx);
152+
}
153+
}
154+
155+
@Override
156+
protected Object clone() throws CloneNotSupportedException {
157+
checkValid();
158+
throw new CloneNotSupportedException("Don't clone test Randoms.");
159+
}
160+
161+
static RuntimeException noSetSeed() {
162+
return new RuntimeException(
163+
"Changing the seed of Random instances is forbidden, it breaks repeatability"
164+
+ " of tests. If you need a mutable instance of Random, create a new (local) instance,"
165+
+ " preferably with the initial seed acquired from this Random instance.");
166+
}
167+
}

randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/Hashing.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.carrotsearch.randomizedtesting.jupiter;
22

3-
public class Hashing {
3+
/** Static hashing utilities. */
4+
public final class Hashing {
5+
/** Bit mixer for {@code long} values. */
46
public static long mix64(long k) {
57
k ^= k >>> 33;
68
k *= 0xff51afd7ed558ccdL;
@@ -10,7 +12,7 @@ public static long mix64(long k) {
1012
return k;
1113
}
1214

13-
/** String hash function redistributing over a long range. */
15+
/** String hash function redistributing over a {@code long}. */
1416
public static long longHash(String v) {
1517
long h = 0;
1618
int length = v.length();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.carrotsearch.randomizedtesting.jupiter;
2+
3+
import java.util.Random;
4+
import java.util.function.LongFunction;
5+
6+
/** Supplier of {@link Random} instances, given the initial seed value. */
7+
public interface RandomFactory extends LongFunction<Random> {}

randomizedtesting-jupiter/src/main/java/com/carrotsearch/randomizedtesting/jupiter/RandomizedContext.java

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,39 @@
22

33
import java.util.ArrayList;
44
import java.util.Collections;
5-
import java.util.Locale;
65
import java.util.Objects;
76
import java.util.Random;
8-
import java.util.function.LongFunction;
97
import org.junit.jupiter.api.extension.ExtensionContext;
108

119
public final class RandomizedContext {
1210
private final RandomizedContext parent;
13-
private final Thread owner;
1411
private final Seed seed;
1512
final String contextId;
1613

1714
private final SeedChain remainingSeedChain;
1815

1916
private final Random random;
20-
private final LongFunction<Random> seedToRandomFn;
17+
private final RandomFactory randomFactory;
2118

2219
RandomizedContext(
2320
String contextId,
2421
RandomizedContext parent,
25-
Thread owner,
26-
LongFunction<Random> seedToRandomFn,
22+
RandomFactory randomFactory,
2723
Seed seed,
2824
SeedChain remainingSeedChain) {
2925
this.contextId = contextId;
3026
this.parent = parent;
31-
this.owner = owner;
3227
this.remainingSeedChain = remainingSeedChain;
33-
this.seedToRandomFn = seedToRandomFn;
28+
this.randomFactory = randomFactory;
3429

3530
assert !seed.isUnspecified();
3631
this.seed = seed;
37-
this.random = seedToRandomFn.apply(seed.value());
32+
this.random = randomFactory.apply(seed.value());
3833
}
3934

4035
@Override
4136
public String toString() {
42-
return "Randomized context ["
43-
+ ("seedChain=" + getSeedChain() + ",")
44-
+ ("thread=" + Threads.threadName(owner))
45-
+ "]";
37+
return "Randomized context [" + ("seedChain=" + getSeedChain() + ",") + "]";
4638
}
4739

4840
SeedChain getSeedChain() {
@@ -67,20 +59,10 @@ private RandomizedContext getParent() {
6759
}
6860

6961
public Random getRandom() {
70-
if (Thread.currentThread() != owner) {
71-
throw new RuntimeException(
72-
String.format(
73-
Locale.ROOT,
74-
"This %s instance is bound to thread %s, can't access it from thread: %s",
75-
RandomizedContext.class.getName(),
76-
owner,
77-
Thread.currentThread()));
78-
}
79-
8062
return random;
8163
}
8264

83-
RandomizedContext deriveNew(Thread thread, ExtensionContext extensionContext) {
65+
RandomizedContext deriveNew(ExtensionContext extensionContext) {
8466
// sanity check.
8567
{
8668
var id = extensionContext.getUniqueId();
@@ -99,11 +81,6 @@ RandomizedContext deriveNew(Thread thread, ExtensionContext extensionContext) {
9981
}
10082

10183
return new RandomizedContext(
102-
extensionContext.getUniqueId(),
103-
this,
104-
thread,
105-
seedToRandomFn,
106-
nextSeed,
107-
firstAndRest.rest());
84+
extensionContext.getUniqueId(), this, randomFactory, nextSeed, firstAndRest.rest());
10885
}
10986
}

0 commit comments

Comments
 (0)