Skip to content

Commit 3ab3bbe

Browse files
authored
Merge pull request #3 from randomizedtesting/random-injection-asserting-random
Add direct Random parameter injection. Add AssertingRandom and synchronization-less Random factories
2 parents 6f17547 + 9c2bd57 commit 3ab3bbe

9 files changed

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

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> {}
Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,42 @@
11
package com.carrotsearch.randomizedtesting.jupiter;
22

3+
import java.io.Closeable;
4+
import java.io.IOException;
35
import java.util.ArrayList;
46
import java.util.Collections;
5-
import java.util.Locale;
67
import java.util.Objects;
78
import java.util.Random;
8-
import java.util.function.LongFunction;
99
import org.junit.jupiter.api.extension.ExtensionContext;
1010

11-
public final class RandomizedContext {
11+
public final class RandomizedContext implements Closeable {
1212
private final RandomizedContext parent;
13-
private final Thread owner;
1413
private final Seed seed;
1514
final String contextId;
1615

1716
private final SeedChain remainingSeedChain;
1817

1918
private final Random random;
20-
private final LongFunction<Random> seedToRandomFn;
19+
private final RandomFactory randomFactory;
2120

2221
RandomizedContext(
2322
String contextId,
2423
RandomizedContext parent,
25-
Thread owner,
26-
LongFunction<Random> seedToRandomFn,
24+
RandomFactory randomFactory,
2725
Seed seed,
2826
SeedChain remainingSeedChain) {
2927
this.contextId = contextId;
3028
this.parent = parent;
31-
this.owner = owner;
3229
this.remainingSeedChain = remainingSeedChain;
33-
this.seedToRandomFn = seedToRandomFn;
30+
this.randomFactory = randomFactory;
3431

3532
assert !seed.isUnspecified();
3633
this.seed = seed;
37-
this.random = seedToRandomFn.apply(seed.value());
34+
this.random = randomFactory.apply(seed.value());
3835
}
3936

4037
@Override
4138
public String toString() {
42-
return "Randomized context ["
43-
+ ("seedChain=" + getSeedChain() + ",")
44-
+ ("thread=" + Threads.threadName(owner))
45-
+ "]";
39+
return "Randomized context [" + ("seedChain=" + getSeedChain() + ",") + "]";
4640
}
4741

4842
SeedChain getSeedChain() {
@@ -67,20 +61,10 @@ private RandomizedContext getParent() {
6761
}
6862

6963
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-
8064
return random;
8165
}
8266

83-
RandomizedContext deriveNew(Thread thread, ExtensionContext extensionContext) {
67+
RandomizedContext deriveNew(ExtensionContext extensionContext) {
8468
// sanity check.
8569
{
8670
var id = extensionContext.getUniqueId();
@@ -99,11 +83,13 @@ RandomizedContext deriveNew(Thread thread, ExtensionContext extensionContext) {
9983
}
10084

10185
return new RandomizedContext(
102-
extensionContext.getUniqueId(),
103-
this,
104-
thread,
105-
seedToRandomFn,
106-
nextSeed,
107-
firstAndRest.rest());
86+
extensionContext.getUniqueId(), this, randomFactory, nextSeed, firstAndRest.rest());
87+
}
88+
89+
@Override
90+
public void close() throws IOException {
91+
if (random instanceof Closeable c) {
92+
c.close();
93+
}
10894
}
10995
}

0 commit comments

Comments
 (0)