diff --git a/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/random/DeterministicRandom.java b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/random/DeterministicRandom.java new file mode 100644 index 000000000..d14e61e82 --- /dev/null +++ b/fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/random/DeterministicRandom.java @@ -0,0 +1,70 @@ +/* + * Fixture Monkey + * + * Copyright (c) 2021-present NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.fixturemonkey.api.random; + +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +@API(since = "1.1.16", status = Status.MAINTAINED) +public class DeterministicRandom extends Random { + private static final Map SEED_CACHE = new ConcurrentHashMap<>(); + private static final int MAX_CACHE_SIZE = 32; + + private long currentSeed; + + public DeterministicRandom(long seed) { + super(seed); + this.currentSeed = seed; + } + + @Override + public long nextLong() { + long value = super.nextLong(); + this.currentSeed = value; + + if (!SEED_CACHE.containsKey(value)) { + manageCacheSize(); + SEED_CACHE.put(value, new Random(value)); + } + + return value; + } + + public Random getRandomInstance(long seed) { + return SEED_CACHE.get(seed); + } + + public Random getCurrentSeedRandom() { + return SEED_CACHE.get(this.currentSeed); + } + + private static void manageCacheSize() { + if (SEED_CACHE.size() >= MAX_CACHE_SIZE) { + SEED_CACHE.clear(); + } + } + + public static Map getSeedCache() { + return SEED_CACHE; + } +} diff --git a/fixture-monkey-api/src/test/java/com/navercorp/fixturemonkey/api/random/DeterministicRandomTest.java b/fixture-monkey-api/src/test/java/com/navercorp/fixturemonkey/api/random/DeterministicRandomTest.java new file mode 100644 index 000000000..49b5854d9 --- /dev/null +++ b/fixture-monkey-api/src/test/java/com/navercorp/fixturemonkey/api/random/DeterministicRandomTest.java @@ -0,0 +1,127 @@ +package com.navercorp.fixturemonkey.api.random; + +import static org.assertj.core.api.BDDAssertions.then; + +import java.util.Random; + +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.constraints.LongRange; + +class DeterministicRandomTest { + + @Property + void nextLongCreatesRandomInstanceInCache(@ForAll @LongRange(min = 1L) long initialSeed) { + // given + DeterministicRandom.getSeedCache().clear(); + DeterministicRandom deterministicRandom = new DeterministicRandom(initialSeed); + + // when + long seed = deterministicRandom.nextLong(); + + // then + then(DeterministicRandom.getSeedCache()).containsKey(seed); + then(DeterministicRandom.getSeedCache().get(seed)).isNotNull(); + } + + @Property + void getRandomInstanceRetrievesCachedInstance(@ForAll @LongRange(min = 1L) long initialSeed) { + // given + DeterministicRandom.getSeedCache().clear(); + DeterministicRandom deterministicRandom = new DeterministicRandom(initialSeed); + long seed = deterministicRandom.nextLong(); + + // when + Random cachedRandom = deterministicRandom.getRandomInstance(seed); + + // then + then(cachedRandom).isNotNull(); + then(cachedRandom).isSameAs(DeterministicRandom.getSeedCache().get(seed)); + } + + @Property + void getCurrentSeedRandomReturnsCurrentSeedRandom(@ForAll @LongRange(min = 1L) long initialSeed) { + // given + DeterministicRandom.getSeedCache().clear(); + DeterministicRandom deterministicRandom = new DeterministicRandom(initialSeed); + + // when + long firstSeed = deterministicRandom.nextLong(); + Random firstRandom = deterministicRandom.getCurrentSeedRandom(); + + long secondSeed = deterministicRandom.nextLong(); + Random secondRandom = deterministicRandom.getCurrentSeedRandom(); + + // then + then(firstRandom).isSameAs(deterministicRandom.getRandomInstance(firstSeed)); + then(secondRandom).isSameAs(deterministicRandom.getRandomInstance(secondSeed)); + then(firstRandom).isNotSameAs(secondRandom); + } + + @Property + void cacheSizeIsLimitedTo32(@ForAll @LongRange(min = 1L) long initialSeed) { + // given + DeterministicRandom.getSeedCache().clear(); + DeterministicRandom deterministicRandom = new DeterministicRandom(initialSeed); + + // when + for (int i = 0; i < 35; i++) { + deterministicRandom.nextLong(); + } + + // then + then(DeterministicRandom.getSeedCache().size()).isLessThanOrEqualTo(32); + } + + @Property + void sameInitialSeedProducesSameLongSequence(@ForAll @LongRange(min = 1L) long initialSeed) { + // given + DeterministicRandom.getSeedCache().clear(); + DeterministicRandom random1 = new DeterministicRandom(initialSeed); + DeterministicRandom random2 = new DeterministicRandom(initialSeed); + + // when + long value1 = random1.nextLong(); + long value2 = random2.nextLong(); + + // then + then(value1).isEqualTo(value2); + } + + @Property + void cachedRandomInstanceProducesDeterministicValues(@ForAll @LongRange(min = 1L) long initialSeed) { + // given + DeterministicRandom.getSeedCache().clear(); + DeterministicRandom deterministicRandom = new DeterministicRandom(initialSeed); + long seed = deterministicRandom.nextLong(); + + // when + Random cached1 = deterministicRandom.getRandomInstance(seed); + Random cached2 = deterministicRandom.getRandomInstance(seed); + + // then + then(cached1).isSameAs(cached2); + int value1 = cached1.nextInt(); + int value2 = cached2.nextInt(); + then(value1).isNotEqualTo(value2); // Same instance, so state progresses + } + + @Property + void multipleSeedsAreStoredIndependently(@ForAll @LongRange(min = 1L) long initialSeed) { + // given + DeterministicRandom.getSeedCache().clear(); + DeterministicRandom deterministicRandom = new DeterministicRandom(initialSeed); + + // when + long seed1 = deterministicRandom.nextLong(); + long seed2 = deterministicRandom.nextLong(); + long seed3 = deterministicRandom.nextLong(); + + // then + then(DeterministicRandom.getSeedCache()).containsKeys(seed1, seed2, seed3); + then(deterministicRandom.getRandomInstance(seed1)) + .isNotSameAs(deterministicRandom.getRandomInstance(seed2)); + then(deterministicRandom.getRandomInstance(seed2)) + .isNotSameAs(deterministicRandom.getRandomInstance(seed3)); + } +}