Fix flaky test accord.utils.RandomSourceTest.testBiasedInts (#73)
patch by David Capwell; reviewed by Benedict Elliott Smith for CASSANDRA-19114
diff --git a/accord-core/src/test/java/accord/utils/RandomSourceTest.java b/accord-core/src/test/java/accord/utils/RandomSourceTest.java
index 3d5aaa1..31e2823 100644
--- a/accord-core/src/test/java/accord/utils/RandomSourceTest.java
+++ b/accord-core/src/test/java/accord/utils/RandomSourceTest.java
@@ -19,13 +19,14 @@
package accord.utils;
import java.util.Arrays;
-import java.util.Random;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import static accord.utils.RandomTestRunner.test;
+
public class RandomSourceTest
{
private static final Logger logger = LoggerFactory.getLogger(RandomSourceTest.class);
@@ -33,11 +34,7 @@
@Test
public void testBiasedInts()
{
- RandomSource random = RandomSource.wrap(new Random());
- long seed = random.nextLong();
- logger.info("Seed: {}", seed);
- random.setSeed(seed);
- testBiasedInts(random, 1000, 100000, 0.01, 0.1);
+ test().check(random -> testBiasedInts(random, 1000, 100000, 0.01, 0.1));
}
private void testBiasedInts(RandomSource random, int tests, int perTest, double fudge, double perTestFudge)
@@ -54,7 +51,7 @@
overallDrift /= tests;
Assertions.assertTrue(overallDrift < fudge);
Assertions.assertTrue(overallDrift > -fudge);
- System.out.println(overallDrift);
+ logger.info("{}", overallDrift);
}
private double testOneBiasedInts(RandomSource random, int min, int median, int max, int[] results, double fudge)
@@ -63,9 +60,9 @@
results[i] = random.nextBiasedInt(min, median, max);
Arrays.sort(results);
- int i = Arrays.binarySearch(results, median);
+ int i = ceil(results, median);
if (i < 0) i = -1 - i;
- int j = Arrays.binarySearch(results, median + 1);
+ int j = ceil(results, median + 1);
if (j < 0) j = -2 - j;
else --j;
i -= results.length/2;
@@ -74,18 +71,14 @@
// find minimum distance of the target median value from the actual median value
double distance = Math.abs(i) < Math.abs(j) ? i : j;
double ratio = distance / results.length;
- Assertions.assertTrue(ratio < fudge);
+ Assertions.assertTrue(ratio < fudge, () -> String.format("ratio (%,2f) >= fudge (%,2f); results.length (%,d)", ratio, fudge, results.length));
return ratio;
}
@Test
public void testBiasedLongs()
{
- RandomSource random = RandomSource.wrap(new Random());
- long seed = random.nextLong();
- logger.info("Seed: {}", seed);
- random.setSeed(seed);
- testBiasedLongs(random, 1000, 100000, 0.01, 0.1);
+ test().check(random -> testBiasedLongs(random, 1000, 100000, 0.01, 0.1));
}
private void testBiasedLongs(RandomSource random, int tests, int perTest, double fudge, double perTestFudge)
@@ -102,7 +95,7 @@
overallDrift /= tests;
Assertions.assertTrue(overallDrift < fudge);
Assertions.assertTrue(overallDrift > -fudge);
- System.out.println(overallDrift);
+ logger.info("{}", overallDrift);
}
private double testOneBiasedLongs(RandomSource random, int min, int median, int max, long[] results, double fudge)
@@ -111,18 +104,99 @@
results[i] = random.nextBiasedInt(min, median, max);
Arrays.sort(results);
- int i = Arrays.binarySearch(results, median);
+ int i = ceil(results, median);
if (i < 0) i = -1 - i;
- int j = Arrays.binarySearch(results, median + 1);
+ int j = ceil(results, median + 1);
if (j < 0) j = -2 - j;
else --j;
i -= results.length/2;
j -= results.length/2;
// find minimum distance of the target median value from the actual median value
- double distance = Math.abs(i) < Math.abs(j) ? i : j;
+ double distance = Math.min(i, j);
+
double ratio = distance / results.length;
- Assertions.assertTrue(ratio < fudge);
- return ratio;
+ Assertions.assertTrue(ratio < fudge, () -> String.format("ratio (%,2f) >= fudge (%,2f); results.length (%,d)", ratio, fudge, results.length));
+ return distance / results.length;
+ }
+
+ private static int ceil(int[] array, int target)
+ {
+ return ceil(array, target, 0, array.length);
+ }
+
+ /**
+ * Yields the minimum index in the range <code>a[fromIndex, toIndex)</code> containing a value that is greater than or equal to the provided key.
+ * The method requires (but does not check) that the range is sorted in ascending order; a result of toIndex indicates no value greater than or
+ * equal to the key exists in the range
+ *
+ * @param a list to look in, where this.isOrdered(a) holds
+ * @param key key to find
+ * @param fromIndex first index to look in
+ * @param toIndex first index to exclude from search (i.e. exclusive upper bound)
+ * @return minimum index in the range containing a value that is greater than or equal to the provided key
+ */
+ private static int ceil(final int[] a, final int key, final int fromIndex, final int toIndex)
+ {
+ // This was taken from https://github.com/belliottsmith/jjoost/blob/0b40ae494af408dfecd4527ac9e9d1ec323315e3/jjoost-base/src/org/jjoost/util/order/IntOrder.java#L80
+ int i = fromIndex - 1;
+ int j = toIndex;
+
+ while (i < j - 1)
+ {
+
+ // { a[i] < v ^ a[j] >= v }
+
+ final int m = (i + j) >>> 1;
+ final long v = a[m];
+
+ if (v >= key) j = m;
+ else i = m;
+
+ // { a[m] >= v => a[j] >= v => a[i] < v ^ a[j] >= v }
+ // { a[m] < v => a[i] < v => a[i] < v ^ a[j] >= v }
+
+ }
+ return j;
+ }
+
+ private static int ceil(long[] array, long target)
+ {
+ return ceil(array, target, 0, array.length);
+ }
+
+ /**
+ * Yields the minimum index in the range <code>a[fromIndex, toIndex)</code> containing a value that is greater than or equal to the provided key.
+ * The method requires (but does not check) that the range is sorted in ascending order; a result of toIndex indicates no value greater than or
+ * equal to the key exists in the range
+ *
+ * @param a list to look in, where this.isOrdered(a) holds
+ * @param key key to find
+ * @param fromIndex first index to look in
+ * @param toIndex first index to exclude from search (i.e. exclusive upper bound)
+ * @return minimum index in the range containing a value that is greater than or equal to the provided key
+ */
+ private static int ceil(final long[] a, final long key, final int fromIndex, final int toIndex)
+ {
+ // This was taken from https://github.com/belliottsmith/jjoost/blob/0b40ae494af408dfecd4527ac9e9d1ec323315e3/jjoost-base/src/org/jjoost/util/order/IntOrder.java#L80
+ int i = fromIndex - 1;
+ int j = toIndex;
+
+ while (i < j - 1)
+ {
+
+ // { a[i] < v ^ a[j] >= v }
+
+ final int m = (i + j) >>> 1;
+ final long v = a[m];
+
+ if (v >= key) j = m;
+ else i = m;
+
+ // { a[m] >= v => a[j] >= v => a[i] < v ^ a[j] >= v }
+ // { a[m] < v => a[i] < v => a[i] < v ^ a[j] >= v }
+
+ }
+ return j;
}
}
diff --git a/accord-core/src/test/java/accord/utils/RandomTestRunner.java b/accord-core/src/test/java/accord/utils/RandomTestRunner.java
new file mode 100644
index 0000000..9a695f6
--- /dev/null
+++ b/accord-core/src/test/java/accord/utils/RandomTestRunner.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 accord.utils;
+
+import java.util.function.Consumer;
+
+/**
+ * The main difference between this class and {@link Property} is that this class does not reference {@link Gen} and some
+ * authors wish to avoid that interface, but need to correctly work with randomness in a test that is reproducable when
+ * issues are found from CI.
+ *
+ * {@link Builder#check(Consumer)} is functinally the same as the following from {@link Property}
+ *
+ * {@code qt().withExamples(1).check(testcase); }
+ */
+public class RandomTestRunner
+{
+ public static Builder test()
+ {
+ return new Builder();
+ }
+
+ public static class Builder
+ {
+ private Long seed = null;
+
+ /**
+ * When a test failure is detected, set the seed for the test using this method to cause it to get repeated
+ */
+ @SuppressWarnings("unused")
+ public Builder withSeed(long seed)
+ {
+ this.seed = seed;
+ return this;
+ }
+
+ public void check(Consumer<RandomSource> fn)
+ {
+ RandomSource random = new DefaultRandom();
+ if (seed == null)
+ seed = random.nextLong();
+ random.setSeed(seed);
+ try
+ {
+ fn.accept(random);
+ }
+ catch (Throwable t)
+ {
+ throw new AssertionError("Unexpected error for seed " + seed, t);
+ }
+ }
+ }
+}