Skip to content

Commit 1f7203c

Browse files
authored
Merge pull request #4 from randomizedtesting/fix-seed-annotation
Add FixSeed annotation
2 parents 3ab3bbe + 10ed2a0 commit 1f7203c

6 files changed

Lines changed: 245 additions & 12 deletions

File tree

etc/junit4-missing-features.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[ai generated overview of junit4 features]
22

3-
4. Shuffled test execution order and seed annotations
3+
4. seed annotations
44
- @Seed on a class fixes the main seed, making execution fully deterministic
55
- @Seeds / @Seed on a method pins a per-method seed for regression coverage
66
while still running once with a fresh random seed
@@ -44,3 +44,9 @@
4444
- Utility methods on RandomizedTest: randomInt(), randomIntBetween(),
4545
randomBoolean(), randomFloat(), etc.
4646
- Encourages testing over a broad input domain rather than fixed values
47+
48+
[possibly doable with a custom test engine]
49+
50+
- predictably shuffled test execution order
51+
- blowing up test reps using tests.iters
52+
-
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.carrotsearch.randomizedtesting.jupiter;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
10+
/**
11+
* This annotation should be placed on classes or methods that are {@link Randomized} and would like
12+
* to use a constant seed (for reproducing a problem or other reasons).
13+
*
14+
* <p>Note that seed fixing is always possible by setting {@link
15+
* com.carrotsearch.randomizedtesting.jupiter.RandomizedContextSupplier.SysProps#TESTS_SEED} system
16+
* property, this is just convenience.
17+
*/
18+
@Target({ElementType.TYPE, ElementType.METHOD})
19+
@Documented
20+
@Retention(RetentionPolicy.RUNTIME)
21+
@ExtendWith(RandomizedContextSupplier.class)
22+
public @interface FixSeed {
23+
String value();
24+
}

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.io.IOException;
55
import java.util.ArrayList;
66
import java.util.Collections;
7+
import java.util.Locale;
78
import java.util.Objects;
89
import java.util.Random;
910
import org.junit.jupiter.api.extension.ExtensionContext;
@@ -76,14 +77,33 @@ RandomizedContext deriveNew(ExtensionContext extensionContext) {
7677
}
7778
}
7879

79-
var firstAndRest = this.remainingSeedChain.pop();
80+
SeedChain seedChain;
81+
var annotationSeed = extensionContext.getElement().map(e -> e.getAnnotation(FixSeed.class));
82+
if (annotationSeed.isPresent()) {
83+
seedChain = SeedChain.parse(annotationSeed.get().value());
84+
for (var seed : seedChain.seeds()) {
85+
if (seed.isUnspecified()) {
86+
throw new RuntimeException(
87+
String.format(
88+
Locale.ROOT,
89+
"@%s annotatoin must declare concrete seeds or seed chains on: %s",
90+
FixSeed.class.getName(),
91+
extensionContext.getElement().get()));
92+
}
93+
}
94+
} else {
95+
seedChain = this.remainingSeedChain;
96+
}
97+
98+
var firstAndRest = seedChain.pop();
8099
var nextSeed = firstAndRest.first();
100+
var remainingChain = firstAndRest.rest();
81101
if (nextSeed.isUnspecified()) {
82102
nextSeed = new Seed(this.seed.value() ^ Hashing.longHash(extensionContext.getUniqueId()));
83103
}
84104

85105
return new RandomizedContext(
86-
extensionContext.getUniqueId(), this, randomFactory, nextSeed, firstAndRest.rest());
106+
extensionContext.getUniqueId(), this, randomFactory, nextSeed, remainingChain);
87107
}
88108

89109
@Override
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.carrotsearch.randomizedtesting.jupiter;
2+
3+
import static com.carrotsearch.randomizedtesting.jupiter.infra.TestInfra.*;
4+
import static org.junit.platform.testkit.engine.EventConditions.*;
5+
6+
import com.carrotsearch.randomizedtesting.jupiter.infra.IgnoreInStandaloneRuns;
7+
import org.assertj.core.api.Assertions;
8+
import org.junit.jupiter.api.Nested;
9+
import org.junit.jupiter.api.RepeatedTest;
10+
import org.junit.jupiter.api.Test;
11+
12+
/** Ensure there is a way to quickly "fix" the seed for the given method or class. */
13+
public class F004_SeedFixing {
14+
@Nested
15+
class TestSeedAnnotation {
16+
@Test
17+
public void testSeedFixing() {
18+
collectExecutionResults(
19+
testKitBuilder(T1.class)
20+
.configurationParameter(
21+
RandomizedContextSupplier.SysProps.TESTS_SEED.propertyKey, "dead:beef:cafe"))
22+
.results()
23+
.allEvents()
24+
.assertThatEvents()
25+
.doNotHave(event(finishedWithFailure()));
26+
}
27+
28+
@Randomized
29+
static class T1 extends IgnoreInStandaloneRuns {
30+
@Test
31+
@FixSeed("babe")
32+
void simpleTest(RandomizedContext ctx) {
33+
Assertions.assertThat(ctx.getSeedChain().toString()).isEqualTo("[DEAD:BEEF:BABE]");
34+
}
35+
}
36+
37+
@Test
38+
public void testClassSeedFixing() {
39+
collectExecutionResults(
40+
testKitBuilder(T2.class)
41+
.configurationParameter(
42+
RandomizedContextSupplier.SysProps.TESTS_SEED.propertyKey, "dead"))
43+
.results()
44+
.allEvents()
45+
.assertThatEvents()
46+
.doNotHave(event(finishedWithFailure()));
47+
}
48+
49+
@Randomized
50+
@FixSeed("babe")
51+
static class T2 extends IgnoreInStandaloneRuns {
52+
@Test
53+
void ta(RandomizedContext ctx) {
54+
Assertions.assertThat(ctx.getSeedChain().toString()).startsWith("[DEAD:BABE:");
55+
}
56+
57+
@Test
58+
void tb(RandomizedContext ctx) {
59+
Assertions.assertThat(ctx.getSeedChain().toString()).startsWith("[DEAD:BABE:");
60+
}
61+
}
62+
63+
@Randomized
64+
@FixSeed("babe:caca")
65+
static class T3 extends IgnoreInStandaloneRuns {
66+
@Test
67+
void ta(RandomizedContext ctx) {
68+
Assertions.assertThat(ctx.getSeedChain().toString()).startsWith("[DEAD:BABE:CACA]");
69+
}
70+
71+
@Test
72+
void tb(RandomizedContext ctx) {
73+
Assertions.assertThat(ctx.getSeedChain().toString()).startsWith("[DEAD:BABE:CACA]");
74+
}
75+
}
76+
77+
@Test
78+
public void testRepeatedTests() {
79+
collectExecutionResults(
80+
testKitBuilder(T4.class)
81+
.configurationParameter(
82+
RandomizedContextSupplier.SysProps.TESTS_SEED.propertyKey, "dead"))
83+
.results()
84+
.allEvents()
85+
.assertThatEvents()
86+
.doNotHave(event(finishedWithFailure()));
87+
}
88+
89+
@Randomized
90+
static class T4 extends IgnoreInStandaloneRuns {
91+
@RepeatedTest(5)
92+
@FixSeed("babe")
93+
void simpleTest(RandomizedContext ctx) {
94+
Assertions.assertThat(ctx.getSeedChain().toString()).endsWith(":BABE]");
95+
}
96+
}
97+
}
98+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Feature: seed fixing using `FixSeed` annotation
2+
3+
## Functionality
4+
5+
* It should be possible to "fix" (make constant, regardless of the
6+
randomization state) the random seed for methods and classes. This can be achieved using `@FixSeed` annotation
7+
8+
```java
9+
10+
@Randomized
11+
@FixSeed("cafebabe")
12+
public class TestClass {
13+
@Test
14+
public void testMethod(Random ctx) {
15+
}
16+
}
17+
```
18+
19+
it is also possible to fix the seed for a particular method, although this allows state randomization at
20+
class level:
21+
22+
```java
23+
24+
@Randomized
25+
public class TestClass {
26+
@Test
27+
@FixSeed("cafebabe")
28+
public void testMethod(Random ctx) {
29+
}
30+
}
31+
```
32+
33+
* The value of the `@FixSeed` annotation can be a single seed or a chain of seeds, affecting nested contexts.
34+
35+
```java
36+
37+
@Randomized
38+
@FixSeed("cafebabe:deadbeef")
39+
public class TestClass {
40+
@Test
41+
public void testMethod(Random ctx) {
42+
}
43+
}
44+
```
45+
46+
* `@FixSeed` can be used to rerun the same tests multiple times with a constant seed or predictably varying seed. For
47+
example, this test runs 5 times with the same seed/ randomness at the test level:
48+
49+
```java
50+
51+
@Randomized
52+
public class TestClass {
53+
@RepeatedTest(5)
54+
@FixSeed("babe")
55+
public void testMethod(Random ctx) {
56+
}
57+
}
58+
```
59+
60+
but this test runs 5 times, each time with a different (but predictable, derived from the parent) seed:
61+
62+
```java
63+
64+
@Randomized
65+
@FixSeed("babe")
66+
public class TestClass {
67+
@RepeatedTest(5)
68+
public void testMethod(Random ctx) {
69+
}
70+
}
71+
```
72+
73+
## Migration notes (from randomizedtesting for junit4)
74+
75+
* `@FixSeed` is renamed from the `@Seed` annotation, used previously.
76+
77+
* There is no way to provide multiple seeds (`@Seeds` annotation), there is no replacement for this functionality.
78+
79+
* There are subtle differences in how the annotation propagates but overall think of the seed annotation placement
80+
(method, class) as affecting the corresponding JUnit5 extension context (its path in the test's UniqueId).

randomizedtesting-jupiter/src/test/java/com/carrotsearch/randomizedtesting/jupiter/experiments/JupiterCallbackMethodOrder.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,37 +33,41 @@
3333
@ValueSource(strings = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"})
3434
public class JupiterCallbackMethodOrder {
3535
static {
36-
System.out.println("Static constructor.");
36+
System.out.println("tclass: static constructor.");
3737
}
3838

3939
public JupiterCallbackMethodOrder(String param) {
40-
System.out.println("constructor: " + param);
40+
System.out.println("tclass: constructor: " + param);
4141
}
4242

4343
@BeforeAll
4444
public static void beforeAll() {
45-
System.out.println("Before all.");
45+
System.out.println("tclass: beforeAll.");
4646
}
4747

4848
@AfterAll
4949
public static void afterAll() {
50-
System.out.println("After all.");
50+
System.out.println("tclass: afterAll.");
5151
}
5252

5353
@BeforeEach
54-
public void before() {}
54+
public void before() {
55+
System.out.println("tclass: beforeEach.");
56+
}
5557

5658
@AfterEach
57-
public void after() {}
59+
public void after() {
60+
System.out.println("tclass: afterEach.");
61+
}
5862

5963
@Test
6064
public void b() {
61-
System.out.println("Test b.");
65+
System.out.println("tclass: test b.");
6266
}
6367

6468
@Test
6569
public void a() {
66-
System.out.println("Test a.");
70+
System.out.println("tclass: test a.");
6771
}
6872

6973
public static class DebugExt
@@ -80,7 +84,8 @@ public static class DebugExt
8084
InvocationInterceptor {
8185

8286
private static void log(String callback, ExtensionContext context) {
83-
System.out.println(callback + " | " + context.getUniqueId());
87+
System.out.println(
88+
"ext: " + callback + "\n " + context.getUniqueId() + "\n " + context.getElement());
8489
}
8590

8691
@Override

0 commit comments

Comments
 (0)