Skip to content

Commit 84e0308

Browse files
committed
refactor: replace concrete DeterministicRandom with RandomSource interface
1 parent dc72f84 commit 84e0308

File tree

5 files changed

+292
-197
lines changed

5 files changed

+292
-197
lines changed

fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/random/DeterministicRandom.java

Lines changed: 0 additions & 70 deletions
This file was deleted.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Fixture Monkey
3+
*
4+
* Copyright (c) 2021-present NAVER Corp.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.navercorp.fixturemonkey.api.random;
20+
21+
import java.util.LinkedHashMap;
22+
import java.util.Map;
23+
import java.util.Random;
24+
25+
import org.apiguardian.api.API;
26+
import org.apiguardian.api.API.Status;
27+
28+
/**
29+
* A RandomSource implementation that caches random instances by seed values.
30+
* This enables deterministic random generation by reusing random instances for the same seed.
31+
* The cache is limited to a maximum of 32 entries to prevent memory leaks.
32+
*
33+
* @since 1.1.16
34+
*/
35+
@API(since = "1.1.16", status = Status.EXPERIMENTAL)
36+
public final class DeterministicRandomSource implements RandomSource {
37+
private static final int MAX_CACHE_SIZE = 32;
38+
39+
private final Map<Long, Random> seedCache;
40+
private final Random baseRandom;
41+
private long currentSeed;
42+
43+
/**
44+
* Creates a new DeterministicRandomSource with the given initial seed.
45+
*
46+
* @param initialSeed the initial seed value
47+
*/
48+
public DeterministicRandomSource(long initialSeed) {
49+
this.baseRandom = new Random(initialSeed);
50+
this.currentSeed = initialSeed;
51+
this.seedCache = new LinkedHashMap<Long, Random>(MAX_CACHE_SIZE, 0.75f, true) {
52+
@Override
53+
protected boolean removeEldestEntry(Map.Entry<Long, Random> eldest) {
54+
return size() > MAX_CACHE_SIZE;
55+
}
56+
};
57+
}
58+
59+
@Override
60+
public long nextSeed() {
61+
long seed = this.baseRandom.nextLong();
62+
this.currentSeed = seed;
63+
64+
this.seedCache.computeIfAbsent(seed, Random::new);
65+
66+
return seed;
67+
}
68+
69+
@Override
70+
public Random getRandom(long seed) {
71+
return this.seedCache.get(seed);
72+
}
73+
74+
@Override
75+
public Random getCurrentSeedRandom() {
76+
return this.seedCache.get(this.currentSeed);
77+
}
78+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Fixture Monkey
3+
*
4+
* Copyright (c) 2021-present NAVER Corp.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.navercorp.fixturemonkey.api.random;
20+
21+
import java.util.Random;
22+
23+
import org.apiguardian.api.API;
24+
import org.apiguardian.api.API.Status;
25+
26+
/**
27+
* Provides random instances with seed management capabilities.
28+
* Implementations may cache random instances for deterministic behavior.
29+
*
30+
* @since 1.1.16
31+
*/
32+
@API(since = "1.1.16", status = Status.EXPERIMENTAL)
33+
public interface RandomSource {
34+
/**
35+
* Generates and returns the next seed value.
36+
* The implementation may cache a random instance associated with the returned seed.
37+
*
38+
* @return the next seed value
39+
*/
40+
long nextSeed();
41+
42+
/**
43+
* Retrieves a random instance associated with the given seed.
44+
* Returns null if no random instance is associated with the seed.
45+
*
46+
* @param seed the seed value
47+
* @return the random instance associated with the seed, or null if not found
48+
*/
49+
Random getRandom(long seed);
50+
51+
/**
52+
* Retrieves the random instance associated with the current seed.
53+
* The current seed is the last value returned by {@link #nextSeed()}.
54+
*
55+
* @return the random instance associated with the current seed, or null if not found
56+
*/
57+
Random getCurrentSeedRandom();
58+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Fixture Monkey
3+
*
4+
* Copyright (c) 2021-present NAVER Corp.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.navercorp.fixturemonkey.api.random;
20+
21+
import static org.assertj.core.api.BDDAssertions.then;
22+
23+
import java.util.Random;
24+
25+
import net.jqwik.api.ForAll;
26+
import net.jqwik.api.Property;
27+
import net.jqwik.api.constraints.LongRange;
28+
29+
class DeterministicRandomSourceTest {
30+
31+
@Property
32+
void nextSeedCachesRandomInstance(@ForAll @LongRange(min = 1L) long initialSeed) {
33+
// given
34+
RandomSource randomSource = new DeterministicRandomSource(initialSeed);
35+
36+
// when
37+
long seed = randomSource.nextSeed();
38+
39+
// then
40+
Random random = randomSource.getRandom(seed);
41+
then(random).isNotNull();
42+
}
43+
44+
@Property
45+
void getRandomRetrievesCachedInstance(@ForAll @LongRange(min = 1L) long initialSeed) {
46+
// given
47+
RandomSource randomSource = new DeterministicRandomSource(initialSeed);
48+
long seed = randomSource.nextSeed();
49+
50+
// when
51+
Random firstRetrieval = randomSource.getRandom(seed);
52+
Random secondRetrieval = randomSource.getRandom(seed);
53+
54+
// then
55+
then(firstRetrieval).isNotNull();
56+
then(firstRetrieval).isSameAs(secondRetrieval);
57+
}
58+
59+
@Property
60+
void getCurrentSeedRandomReturnsCurrentSeedRandom(@ForAll @LongRange(min = 1L) long initialSeed) {
61+
// given
62+
RandomSource randomSource = new DeterministicRandomSource(initialSeed);
63+
64+
// when
65+
long firstSeed = randomSource.nextSeed();
66+
Random firstRandom = randomSource.getCurrentSeedRandom();
67+
68+
long secondSeed = randomSource.nextSeed();
69+
Random secondRandom = randomSource.getCurrentSeedRandom();
70+
71+
// then
72+
then(firstRandom).isSameAs(randomSource.getRandom(firstSeed));
73+
then(secondRandom).isSameAs(randomSource.getRandom(secondSeed));
74+
then(firstRandom).isNotSameAs(secondRandom);
75+
}
76+
77+
@Property
78+
void manySeedsDoNotCauseMemoryLeak(@ForAll @LongRange(min = 1L) long initialSeed) {
79+
// given
80+
RandomSource randomSource = new DeterministicRandomSource(initialSeed);
81+
82+
// when
83+
for (int i = 0; i < 100; i++) {
84+
randomSource.nextSeed();
85+
}
86+
87+
// then
88+
// If no OutOfMemoryError occurs, the cache is properly managed
89+
then(randomSource).isNotNull();
90+
}
91+
92+
@Property
93+
void sameInitialSeedProducesSameSeedSequence(@ForAll @LongRange(min = 1L) long initialSeed) {
94+
// given
95+
RandomSource randomSource1 = new DeterministicRandomSource(initialSeed);
96+
RandomSource randomSource2 = new DeterministicRandomSource(initialSeed);
97+
98+
// when
99+
long seed1 = randomSource1.nextSeed();
100+
long seed2 = randomSource2.nextSeed();
101+
102+
// then
103+
then(seed1).isEqualTo(seed2);
104+
}
105+
106+
@Property
107+
void cachedRandomInstanceProducesDeterministicValues(@ForAll @LongRange(min = 1L) long initialSeed) {
108+
// given
109+
RandomSource randomSource = new DeterministicRandomSource(initialSeed);
110+
long seed = randomSource.nextSeed();
111+
112+
// when
113+
Random cached1 = randomSource.getRandom(seed);
114+
Random cached2 = randomSource.getRandom(seed);
115+
116+
// then
117+
then(cached1).isSameAs(cached2);
118+
int value1 = cached1.nextInt();
119+
int value2 = cached2.nextInt();
120+
then(value1).isNotEqualTo(value2); // Same instance, so state progresses
121+
}
122+
123+
@Property
124+
void multipleSeedsAreStoredIndependently(@ForAll @LongRange(min = 1L) long initialSeed) {
125+
// given
126+
RandomSource randomSource = new DeterministicRandomSource(initialSeed);
127+
128+
// when
129+
long seed1 = randomSource.nextSeed();
130+
long seed2 = randomSource.nextSeed();
131+
long seed3 = randomSource.nextSeed();
132+
133+
// then
134+
Random random1 = randomSource.getRandom(seed1);
135+
Random random2 = randomSource.getRandom(seed2);
136+
Random random3 = randomSource.getRandom(seed3);
137+
138+
then(random1).isNotNull();
139+
then(random2).isNotNull();
140+
then(random3).isNotNull();
141+
then(random1).isNotSameAs(random2);
142+
then(random2).isNotSameAs(random3);
143+
}
144+
145+
@Property
146+
void getRandomReturnsNullForUncachedSeed(@ForAll @LongRange(min = 1L) long initialSeed) {
147+
// given
148+
RandomSource randomSource = new DeterministicRandomSource(initialSeed);
149+
150+
// when
151+
Random random = randomSource.getRandom(999999999L);
152+
153+
// then
154+
then(random).isNull();
155+
}
156+
}

0 commit comments

Comments
 (0)