View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.rng;
18  
19  import java.math.BigDecimal;
20  import java.math.MathContext;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.List;
24  import java.util.SplittableRandom;
25  import java.util.concurrent.ThreadLocalRandom;
26  import java.util.function.LongSupplier;
27  import java.util.stream.Collectors;
28  import java.util.stream.Stream;
29  import org.junit.jupiter.api.Assertions;
30  import org.junit.jupiter.api.Test;
31  import org.junit.jupiter.params.ParameterizedTest;
32  import org.junit.jupiter.params.provider.Arguments;
33  import org.junit.jupiter.params.provider.CsvSource;
34  import org.junit.jupiter.params.provider.MethodSource;
35  import org.junit.jupiter.params.provider.ValueSource;
36  
37  /**
38   * Tests for default method implementations in {@link UniformRandomProvider}.
39   *
40   * <p>This class verifies all exception conditions for the range methods.
41   * Single value generation methods are asserted using a test of uniformity
42   * from multiple samples.
43   *
44   * <p>Stream methods are tested {@link UniformRandomProviderStreamTest}.
45   */
46  class UniformRandomProviderTest {
47      /** Sample size for statistical uniformity tests. */
48      private static final int SAMPLE_SIZE = 1000;
49      /** Sample size for statistical uniformity tests as a BigDecimal. */
50      private static final BigDecimal SAMPLE_SIZE_BD = BigDecimal.valueOf(SAMPLE_SIZE);
51      /** Relative error used to verify the sum of expected frequencies matches the sample size. */
52      private static final double RELATIVE_ERROR = Math.ulp(1.0) * 2;
53  
54      /**
55       * Dummy class for checking the behavior of the UniformRandomProvider.
56       */
57      private static class DummyGenerator implements UniformRandomProvider {
58          /** An instance. */
59          static final DummyGenerator INSTANCE = new DummyGenerator();
60  
61          @Override
62          public long nextLong() {
63              throw new UnsupportedOperationException("The nextLong method should not be invoked");
64          }
65      }
66  
67      static int[] invalidNextIntBound() {
68          return new int[] {0, -1, Integer.MIN_VALUE};
69      }
70  
71      static Stream<Arguments> invalidNextIntOriginBound() {
72          return Stream.of(
73              Arguments.of(1, 1),
74              Arguments.of(2, 1),
75              Arguments.of(-1, -1),
76              Arguments.of(-1, -2)
77          );
78      }
79  
80      static long[] invalidNextLongBound() {
81          return new long[] {0, -1, Long.MIN_VALUE};
82      }
83  
84      static Stream<Arguments> invalidNextLongOriginBound() {
85          return Stream.of(
86              Arguments.of(1L, 1L),
87              Arguments.of(2L, 1L),
88              Arguments.of(-1L, -1L),
89              Arguments.of(-1L, -2L)
90          );
91      }
92  
93      static float[] invalidNextFloatBound() {
94          return new float[] {0, -1, -Float.MAX_VALUE, Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY};
95      }
96  
97      static Stream<Arguments> invalidNextFloatOriginBound() {
98          return Stream.of(
99              Arguments.of(1f, 1f),
100             Arguments.of(2f, 1f),
101             Arguments.of(-1f, -1f),
102             Arguments.of(-1f, -2f),
103             Arguments.of(Float.NEGATIVE_INFINITY, 0f),
104             Arguments.of(0f, Float.POSITIVE_INFINITY),
105             Arguments.of(0f, Float.NaN),
106             Arguments.of(Float.NaN, 1f)
107         );
108     }
109 
110     static double[] invalidNextDoubleBound() {
111         return new double[] {0, -1, -Double.MAX_VALUE, Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY};
112     }
113 
114     static Stream<Arguments> invalidNextDoubleOriginBound() {
115         return Stream.of(
116             Arguments.of(1, 1),
117             Arguments.of(2, 1),
118             Arguments.of(-1, -1),
119             Arguments.of(-1, -2),
120             Arguments.of(Double.NEGATIVE_INFINITY, 0),
121             Arguments.of(0, Double.POSITIVE_INFINITY),
122             Arguments.of(0, Double.NaN),
123             Arguments.of(Double.NaN, 1)
124         );
125     }
126 
127     /**
128      * Creates a functional random generator by implementing the
129      * {@link UniformRandomProvider#nextLong} method with a high quality source of randomness.
130      *
131      * @param seed Seed for the generator.
132      * @return the random generator
133      */
134     private static UniformRandomProvider createRNG(long seed) {
135         // The algorithm for SplittableRandom with the default increment passes:
136         // - Test U01 BigCrush
137         // - PractRand with at least 2^42 bytes (4 TiB) of output
138         return new SplittableRandom(seed)::nextLong;
139     }
140 
141     @Test
142     void testNextBytesThrows() {
143         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
144         Assertions.assertThrows(NullPointerException.class, () -> rng.nextBytes(null));
145         Assertions.assertThrows(NullPointerException.class, () -> rng.nextBytes(null, 0, 1));
146         // Invalid range
147         final int length = 10;
148         final byte[] bytes = new byte[length];
149         Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, -1, 1), "start < 0");
150         Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, length, 1), "start >= length");
151         Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 0, -1), "len < 0");
152         Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 5, 10), "start + len > length");
153         Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.nextBytes(bytes, 5, Integer.MAX_VALUE), "start + len > length, taking into account integer overflow");
154     }
155 
156     @ParameterizedTest
157     @MethodSource(value = {"invalidNextIntBound"})
158     void testNextIntBoundThrows(int bound) {
159         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
160         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextInt(bound));
161     }
162 
163     @ParameterizedTest
164     @MethodSource(value = {"invalidNextIntOriginBound"})
165     void testNextIntOriginBoundThrows(int origin, int bound) {
166         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
167         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextInt(origin, bound));
168     }
169 
170     @ParameterizedTest
171     @MethodSource(value = {"invalidNextLongBound"})
172     void testNextLongBoundThrows(long bound) {
173         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
174         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextLong(bound));
175     }
176 
177     @ParameterizedTest
178     @MethodSource(value = {"invalidNextLongOriginBound"})
179     void testNextLongOriginBoundThrows(long origin, long bound) {
180         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
181         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextLong(origin, bound));
182     }
183 
184     @ParameterizedTest
185     @MethodSource(value = {"invalidNextFloatBound"})
186     void testNextFloatBoundThrows(float bound) {
187         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
188         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextFloat(bound));
189     }
190 
191     @ParameterizedTest
192     @MethodSource(value = {"invalidNextFloatOriginBound"})
193     void testNextFloatOriginBoundThrows(float origin, float bound) {
194         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
195         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextFloat(origin, bound));
196     }
197 
198     @ParameterizedTest
199     @MethodSource(value = {"invalidNextDoubleBound"})
200     void testNextDoubleBoundThrows(double bound) {
201         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
202         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextDouble(bound));
203     }
204 
205     @ParameterizedTest
206     @MethodSource(value = {"invalidNextDoubleOriginBound"})
207     void testNextDoubleOriginBoundThrows(double origin, double bound) {
208         final UniformRandomProvider rng = DummyGenerator.INSTANCE;
209         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.nextDouble(origin, bound));
210     }
211 
212     @Test
213     void testNextFloatExtremes() {
214         final UniformRandomProvider rng = new DummyGenerator() {
215             private int i;
216             private final int[] values = {0, -1, 1 << 8};
217             @Override
218             public int nextInt() {
219                 return values[i++];
220             }
221         };
222         Assertions.assertEquals(0, rng.nextFloat(), "Expected zero bits to return 0");
223         Assertions.assertEquals(Math.nextDown(1f), rng.nextFloat(), "Expected all bits to return ~1");
224         // Assumes the value is created from the high 24 bits of nextInt
225         Assertions.assertEquals(1f - Math.nextDown(1f), rng.nextFloat(), "Expected 1 bit (shifted) to return ~0");
226     }
227 
228     @ParameterizedTest
229     @ValueSource(floats = {Float.MIN_NORMAL, Float.MIN_NORMAL / 2})
230     void testNextFloatBoundRounding(float bound) {
231         // This method will be used
232         final UniformRandomProvider rng = new DummyGenerator() {
233             @Override
234             public float nextFloat() {
235                 return Math.nextDown(1.0f);
236             }
237         };
238         Assertions.assertEquals(bound, rng.nextFloat() * bound, "Expected result to be rounded up");
239         Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(bound), "Result was not correctly rounded down");
240     }
241 
242     @Test
243     void testNextFloatOriginBoundInfiniteRange() {
244         // This method will be used
245         final UniformRandomProvider rng = new DummyGenerator() {
246             private int i;
247             private final float[] values = {0, 0.25f, 0.5f, 0.75f, 1};
248             @Override
249             public float nextFloat() {
250                 return values[i++];
251             }
252         };
253         final float x = Float.MAX_VALUE;
254         Assertions.assertEquals(-x, rng.nextFloat(-x, x));
255         Assertions.assertEquals(-x / 2, rng.nextFloat(-x, x), Math.ulp(x / 2));
256         Assertions.assertEquals(0, rng.nextFloat(-x, x));
257         Assertions.assertEquals(x / 2, rng.nextFloat(-x, x), Math.ulp(x / 2));
258         Assertions.assertEquals(Math.nextDown(x), rng.nextFloat(-x, x));
259     }
260 
261     @Test
262     void testNextFloatOriginBoundRounding() {
263         // This method will be used
264         final float v = Math.nextDown(1.0f);
265         final UniformRandomProvider rng = new DummyGenerator() {
266             @Override
267             public float nextFloat() {
268                 return v;
269             }
270         };
271         float origin;
272         float bound;
273 
274         // Simple case
275         origin = 3.5f;
276         bound = 4.5f;
277         Assertions.assertEquals(bound, origin + v * (bound - origin), "Expected result to be rounded up");
278         Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(origin, bound), "Result was not correctly rounded down");
279 
280         // Alternate formula:
281         // requires sub-normal result for 'origin * v' to force rounding
282         origin = -Float.MIN_NORMAL / 2;
283         bound = Float.MIN_NORMAL / 2;
284         Assertions.assertEquals(bound, origin * (1 - v) + v * bound, "Expected result to be rounded up");
285         Assertions.assertEquals(Math.nextDown(bound), rng.nextFloat(origin, bound), "Result was not correctly rounded down");
286     }
287 
288     @Test
289     void testNextDoubleExtremes() {
290         final UniformRandomProvider rng = new DummyGenerator() {
291             private int i;
292             private final long[] values = {0, -1, 1L << 11};
293             @Override
294             public long nextLong() {
295                 return values[i++];
296             }
297         };
298         Assertions.assertEquals(0, rng.nextDouble(), "Expected zero bits to return 0");
299         Assertions.assertEquals(Math.nextDown(1.0), rng.nextDouble(), "Expected all bits to return ~1");
300         // Assumes the value is created from the high 53 bits of nextInt
301         Assertions.assertEquals(1.0 - Math.nextDown(1.0), rng.nextDouble(), "Expected 1 bit (shifted) to return ~0");
302     }
303 
304     @ParameterizedTest
305     @ValueSource(doubles = {Double.MIN_NORMAL, Double.MIN_NORMAL / 2})
306     void testNextDoubleBoundRounding(double bound) {
307         // This method will be used
308         final UniformRandomProvider rng = new DummyGenerator() {
309             @Override
310             public double nextDouble() {
311                 return Math.nextDown(1.0);
312             }
313         };
314         Assertions.assertEquals(bound, rng.nextDouble() * bound, "Expected result to be rounded up");
315         Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(bound), "Result was not correctly rounded down");
316     }
317 
318     @Test
319     void testNextDoubleOriginBoundInfiniteRange() {
320         // This method will be used
321         final UniformRandomProvider rng = new DummyGenerator() {
322             private int i;
323             private final double[] values = {0, 0.25, 0.5, 0.75, 1};
324             @Override
325             public double nextDouble() {
326                 return values[i++];
327             }
328         };
329         final double x = Double.MAX_VALUE;
330         Assertions.assertEquals(-x, rng.nextDouble(-x, x));
331         Assertions.assertEquals(-x / 2, rng.nextDouble(-x, x), Math.ulp(x / 2));
332         Assertions.assertEquals(0, rng.nextDouble(-x, x));
333         Assertions.assertEquals(x / 2, rng.nextDouble(-x, x), Math.ulp(x / 2));
334         Assertions.assertEquals(Math.nextDown(x), rng.nextDouble(-x, x));
335     }
336 
337     @Test
338     void testNextDoubleOriginBoundRounding() {
339         // This method will be used
340         final double v = Math.nextDown(1.0);
341         final UniformRandomProvider rng = new DummyGenerator() {
342             @Override
343             public double nextDouble() {
344                 return v;
345             }
346         };
347         double origin;
348         double bound;
349 
350         // Simple case
351         origin = 3.5;
352         bound = 4.5;
353         Assertions.assertEquals(bound, origin + v * (bound - origin), "Expected result to be rounded up");
354         Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(origin, bound), "Result was not correctly rounded down");
355 
356         // Alternate formula:
357         // requires sub-normal result for 'origin * v' to force rounding
358         origin = -Double.MIN_NORMAL / 2;
359         bound = Double.MIN_NORMAL / 2;
360         Assertions.assertEquals(bound, origin * (1 - v) + v * bound, "Expected result to be rounded up");
361         Assertions.assertEquals(Math.nextDown(bound), rng.nextDouble(origin, bound), "Result was not correctly rounded down");
362     }
363 
364     @Test
365     void testNextBooleanIsIntSignBit() {
366         final int size = 10;
367         final int[] values = ThreadLocalRandom.current().ints(size).toArray();
368         final UniformRandomProvider rng = new DummyGenerator() {
369             private int i;
370             @Override
371             public int nextInt() {
372                 return values[i++];
373             }
374         };
375         for (int i = 0; i < size; i++) {
376             Assertions.assertEquals(values[i] < 0, rng.nextBoolean());
377         }
378     }
379 
380     // Statistical tests for uniform distribution
381 
382     // Monobit tests
383 
384     @ParameterizedTest
385     @ValueSource(longs = {263784628482L, -2563472, -2367482842368L})
386     void testNextIntMonobit(long seed) {
387         final UniformRandomProvider rng = createRNG(seed);
388         int bitCount = 0;
389         final int n = 100;
390         for (int i = 0; i < n; i++) {
391             bitCount += Integer.bitCount(rng.nextInt());
392         }
393         final int numberOfBits = n * Integer.SIZE;
394         assertMonobit(bitCount, numberOfBits);
395     }
396 
397     @ParameterizedTest
398     @ValueSource(longs = {263784628L, 253674, -23568426834L})
399     void testNextDoubleMonobit(long seed) {
400         final UniformRandomProvider rng = createRNG(seed);
401         int bitCount = 0;
402         final int n = 100;
403         for (int i = 0; i < n; i++) {
404             bitCount += Long.bitCount((long) (rng.nextDouble() * (1L << 53)));
405         }
406         final int numberOfBits = n * 53;
407         assertMonobit(bitCount, numberOfBits);
408     }
409 
410     @ParameterizedTest
411     @ValueSource(longs = {265342L, -234232, -672384648284L})
412     void testNextBooleanMonobit(long seed) {
413         final UniformRandomProvider rng = createRNG(seed);
414         int bitCount = 0;
415         final int n = 1000;
416         for (int i = 0; i < n; i++) {
417             if (rng.nextBoolean()) {
418                 bitCount++;
419             }
420         }
421         final int numberOfBits = n;
422         assertMonobit(bitCount, numberOfBits);
423     }
424 
425     @ParameterizedTest
426     @ValueSource(longs = {-1526731, 263846, 4545})
427     void testNextFloatMonobit(long seed) {
428         final UniformRandomProvider rng = createRNG(seed);
429         int bitCount = 0;
430         final int n = 100;
431         for (int i = 0; i < n; i++) {
432             bitCount += Integer.bitCount((int) (rng.nextFloat() * (1 << 24)));
433         }
434         final int numberOfBits = n * 24;
435         assertMonobit(bitCount, numberOfBits);
436     }
437 
438     @ParameterizedTest
439     @CsvSource({
440         "-2638468223894, 16",
441         "235647674, 17",
442         "-928738475, 23",
443     })
444     void testNextBytesMonobit(long seed, int range) {
445         final UniformRandomProvider rng = createRNG(seed);
446         final byte[] bytes = new byte[range];
447         int bitCount = 0;
448         final int n = 100;
449         for (int i = 0; i < n; i++) {
450             rng.nextBytes(bytes);
451             for (final byte b1 : bytes) {
452                 bitCount += Integer.bitCount(b1 & 0xff);
453             }
454         }
455         final int numberOfBits = n * Byte.SIZE * range;
456         assertMonobit(bitCount, numberOfBits);
457     }
458 
459     /**
460      * Assert that the number of 1 bits is approximately 50%. This is based upon a
461      * fixed-step "random walk" of +1/-1 from zero.
462      *
463      * <p>The test is equivalent to the NIST Monobit test with a fixed p-value of
464      * 0.01. The number of bits is recommended to be above 100.</p>
465      *
466      * @see <A
467      * href="https://csrc.nist.gov/publications/detail/sp/800-22/rev-1a/final">Bassham,
468      * et al (2010) NIST SP 800-22: A Statistical Test Suite for Random and
469      * Pseudorandom Number Generators for Cryptographic Applications. Section
470      * 2.1.</a>
471      *
472      * @param bitCount The bit count.
473      * @param numberOfBits Number of bits.
474      */
475     private static void assertMonobit(int bitCount, int numberOfBits) {
476         // Convert the bit count into a number of +1/-1 steps.
477         final double sum = 2.0 * bitCount - numberOfBits;
478         // The reference distribution is Normal with a standard deviation of sqrt(n).
479         // Check the absolute position is not too far from the mean of 0 with a fixed
480         // p-value of 0.01 taken from a 2-tailed Normal distribution. Computation of
481         // the p-value requires the complimentary error function.
482         final double absSum = Math.abs(sum);
483         final double max = Math.sqrt(numberOfBits) * 2.5758293035489004;
484         Assertions.assertTrue(absSum <= max,
485             () -> "Walked too far astray: " + absSum + " > " + max +
486                   " (test will fail randomly about 1 in 100 times)");
487     }
488 
489     // Uniformity tests
490 
491     @ParameterizedTest
492     @CsvSource({
493         "263746283, 23, 0, 23",
494         "-126536861889, 16, 0, 16",
495         "617868181124, 1234, 567, 89",
496         "-56788, 512, 0, 233",
497         "6787535424, 512, 233, 279",
498     })
499     void testNextBytesUniform(long seed,
500                               int length, int start, int size) {
501         final UniformRandomProvider rng = createRNG(seed);
502         final byte[] buffer = new byte[length];
503 
504         final Runnable nextMethod = start == 0 && size == length ?
505                 () -> rng.nextBytes(buffer) :
506                 () -> rng.nextBytes(buffer, start, size);
507 
508         final int last = start + size;
509         Assertions.assertTrue(isUniformNextBytes(buffer, start, last, nextMethod),
510                               "Expected uniform bytes");
511 
512         // The parts of the buffer where no values are put should be zero.
513         for (int i = 0; i < start; i++) {
514             Assertions.assertEquals(0, buffer[i], () -> "Filled < start: " + start);
515         }
516         for (int i = last; i < length; i++) {
517             Assertions.assertEquals(0, buffer[i], () -> "Filled >= last: " + last);
518         }
519     }
520 
521     /**
522      * Checks that the generator values can be placed into 256 bins with
523      * approximately equal number of counts.
524      * Test allows to select only part of the buffer for performing the
525      * statistics.
526      *
527      * @param buffer Buffer to be filled.
528      * @param first First element (included) of {@code buffer} range for
529      * which statistics must be taken into account.
530      * @param last Last element (excluded) of {@code buffer} range for
531      * which statistics must be taken into account.
532      * @param nextMethod Method that fills the given {@code buffer}.
533      * @return {@code true} if the distribution is uniform.
534      */
535     private static boolean isUniformNextBytes(byte[] buffer,
536                                               int first,
537                                               int last,
538                                               Runnable nextMethod) {
539         final int sampleSize = 10000;
540 
541         // Number of possible values (do not change).
542         final int byteRange = 256;
543         // Chi-square critical value with 255 degrees of freedom
544         // and 1% significance level.
545         final double chi2CriticalValue = 310.45738821990585;
546 
547         // Bins.
548         final long[] observed = new long[byteRange];
549         final double[] expected = new double[byteRange];
550 
551         Arrays.fill(expected, sampleSize * (last - first) / (double) byteRange);
552 
553         for (int k = 0; k < sampleSize; k++) {
554             nextMethod.run();
555 
556             for (int i = first; i < last; i++) {
557                 // Convert byte to an index in [0, 255]
558                 ++observed[buffer[i] & 0xff];
559             }
560         }
561 
562         // Compute chi-square.
563         double chi2 = 0;
564         for (int k = 0; k < byteRange; k++) {
565             final double diff = observed[k] - expected[k];
566             chi2 += diff * diff / expected[k];
567         }
568 
569         // Statistics check.
570         return chi2 <= chi2CriticalValue;
571     }
572 
573     // Add range tests
574 
575     @ParameterizedTest
576     @CsvSource({
577         // No lower bound
578         "2673846826, 0, 10",
579         "-23658268, 0, 12",
580         "263478624, 0, 31",
581         "1278332, 0, 32",
582         "99734765, 0, 2016128993",
583         "-63485384, 0, 1834691456",
584         "3876457638, 0, 869657561",
585         "-126784782, 0, 1570504788",
586         "2637846, 0, 2147483647",
587         // Small range
588         "2634682, 567576, 567586",
589         "-56757798989, -1000, -100",
590         "-97324785, -54656, 12",
591         "23423235, -526783468, 257",
592         // Large range
593         "-2634682, -1073741824, 1073741824",
594         "6786868132, -1263842626, 1237846372",
595         "-263846723, -368268, 2147483647",
596         "7352352, -2147483648, 61523457",
597     })
598     void testNextIntUniform(long seed, int origin, int bound) {
599         final UniformRandomProvider rng = createRNG(seed);
600         final LongSupplier nextMethod = origin == 0 ?
601                 () -> rng.nextInt(bound) :
602                 () -> rng.nextInt(origin, bound);
603         checkNextInRange("nextInt", origin, bound, nextMethod);
604     }
605 
606     @ParameterizedTest
607     @CsvSource({
608         // No lower bound
609         "2673846826, 0, 11",
610         "-23658268, 0, 19",
611         "263478624, 0, 31",
612         "1278332, 0, 32",
613         "99734765, 0, 2326378468282368421",
614         "-63485384, 0, 4872347624242243222",
615         "3876457638, 0, 6263784682638866843",
616         "-126784782, 0, 7256684297832668332",
617         "2637846, 0, 9223372036854775807",
618         // Small range
619         "2634682, 567576, 567586",
620         "-56757798989, -1000, -100",
621         "-97324785, -54656, 12",
622         "23423235, -526783468, 257",
623         // Large range
624         "-2634682, -4611686018427387904, 4611686018427387904",
625         "6786868132, -4962836478223688590, 6723648246224929947",
626         "-263846723, -368268, 9223372036854775807",
627         "7352352, -9223372036854775808, 61523457",
628     })
629     void testNextLongUniform(long seed, long origin, long bound) {
630         final UniformRandomProvider rng = createRNG(seed);
631         final LongSupplier nextMethod = origin == 0 ?
632                 () -> rng.nextLong(bound) :
633                 () -> rng.nextLong(origin, bound);
634         checkNextInRange("nextLong", origin, bound, nextMethod);
635     }
636 
637     @ParameterizedTest
638     @CsvSource({
639         // Note: If the range limits are integers above 2^24 (16777216) it is not possible
640         // to represent all the values with a float. This has no effect on sampling into bins
641         // but should be avoided when generating integers for use in production code.
642 
643         // No lower bound.
644         "2673846826, 0, 11",
645         "-23658268, 0, 19",
646         "263478624, 0, 31",
647         "1278332, 0, 32",
648         "99734765, 0, 1234",
649         "-63485384, 0, 578",
650         "3876457638, 0, 10000",
651         "-126784782, 0, 2983423",
652         "2637846, 0, 16777216",
653         // Range
654         "2634682, 567576, 567586",
655         "-56757798989, -1000, -100",
656         "-97324785, -54656, 12",
657         "23423235, -526783468, 257",
658         "-2634682, -688689797, -516827",
659         "6786868132, -67, 67",
660         "-263846723, -5678, 42",
661         "7352352, 678687, 61523457",
662     })
663     void testNextFloatUniform(long seed, float origin, float bound) {
664         Assertions.assertEquals((long) origin, origin, "origin");
665         Assertions.assertEquals((long) bound, bound, "bound");
666         final UniformRandomProvider rng = createRNG(seed);
667         // Note casting as long will round towards zero.
668         // If the upper bound is negative then this can create a domain error so use floor.
669         final LongSupplier nextMethod = origin == 0 ?
670                 () -> (long) rng.nextFloat(bound) :
671                 () -> (long) Math.floor(rng.nextFloat(origin, bound));
672         checkNextInRange("nextFloat", (long) origin, (long) bound, nextMethod);
673     }
674 
675 
676     @ParameterizedTest
677     @CsvSource({
678         // Note: If the range limits are integers above 2^53 (9007199254740992) it is not possible
679         // to represent all the values with a double. This has no effect on sampling into bins
680         // but should be avoided when generating integers for use in production code.
681 
682         // No lower bound.
683         "2673846826, 0, 11",
684         "-23658268, 0, 19",
685         "263478624, 0, 31",
686         "1278332, 0, 32",
687         "99734765, 0, 1234",
688         "-63485384, 0, 578",
689         "3876457638, 0, 10000",
690         "-126784782, 0, 2983423",
691         "2637846, 0, 9007199254740992",
692         // Range
693         "2634682, 567576, 567586",
694         "-56757798989, -1000, -100",
695         "-97324785, -54656, 12",
696         "23423235, -526783468, 257",
697         "-2634682, -688689797, -516827",
698         "6786868132, -67, 67",
699         "-263846723, -5678, 42",
700         "7352352, 678687, 61523457",
701     })
702     void testNextDoubleUniform(long seed, double origin, double bound) {
703         Assertions.assertEquals((long) origin, origin, "origin");
704         Assertions.assertEquals((long) bound, bound, "bound");
705         final UniformRandomProvider rng = createRNG(seed);
706         // Note casting as long will round towards zero.
707         // If the upper bound is negative then this can create a domain error so use floor.
708         final LongSupplier nextMethod = origin == 0 ?
709                 () -> (long) rng.nextDouble(bound) :
710                 () -> (long) Math.floor(rng.nextDouble(origin, bound));
711         checkNextInRange("nextDouble", (long) origin, (long) bound, nextMethod);
712     }
713 
714     /**
715      * Tests uniformity of the distribution produced by the given
716      * {@code nextMethod}.
717      * It performs a chi-square test of homogeneity of the observed
718      * distribution with the expected uniform distribution.
719      * Repeat tests are performed at the 1% level and the total number of failed
720      * tests is tested at the 0.5% significance level.
721      *
722      * @param method Generator method.
723      * @param origin Lower bound (inclusive).
724      * @param bound Upper bound (exclusive).
725      * @param nextMethod method to call.
726      */
727     private static void checkNextInRange(String method,
728                                          long origin,
729                                          long bound,
730                                          LongSupplier nextMethod) {
731         // Do not change
732         // (statistical test assumes that 500 repeats are made with dof = 9).
733         final int numTests = 500;
734         final int numBins = 10; // dof = numBins - 1
735 
736         // Set up bins.
737         final long[] binUpperBounds = new long[numBins];
738         // Range may be above a positive long: step = (bound - origin) / bins
739         final BigDecimal range = BigDecimal.valueOf(bound)
740                 .subtract(BigDecimal.valueOf(origin));
741         final double step = range.divide(BigDecimal.TEN).doubleValue();
742         for (int k = 1; k < numBins; k++) {
743             binUpperBounds[k - 1] = origin + (long) (k * step);
744         }
745         // Final bound
746         binUpperBounds[numBins - 1] = bound;
747 
748         // Create expected frequencies
749         final double[] expected = new double[numBins];
750         long previousUpperBound = origin;
751         final double scale = SAMPLE_SIZE_BD.divide(range, MathContext.DECIMAL128).doubleValue();
752         double sum = 0;
753         for (int k = 0; k < numBins; k++) {
754             final long binWidth = binUpperBounds[k] - previousUpperBound;
755             expected[k] = scale * binWidth;
756             sum += expected[k];
757             previousUpperBound = binUpperBounds[k];
758         }
759         Assertions.assertEquals(SAMPLE_SIZE, sum, SAMPLE_SIZE * RELATIVE_ERROR, "Invalid expected frequencies");
760 
761         final int[] observed = new int[numBins];
762         // Chi-square critical value with 9 degrees of freedom
763         // and 1% significance level.
764         final double chi2CriticalValue = 21.665994333461924;
765 
766         // For storing chi2 larger than the critical value.
767         final List<Double> failedStat = new ArrayList<>();
768         try {
769             final int lastDecileIndex = numBins - 1;
770             for (int i = 0; i < numTests; i++) {
771                 Arrays.fill(observed, 0);
772                 SAMPLE: for (int j = 0; j < SAMPLE_SIZE; j++) {
773                     final long value = nextMethod.getAsLong();
774                     if (value < origin) {
775                         Assertions.fail(String.format("Sample %d not within bound [%d, %d)",
776                                                       value, origin, bound));
777                     }
778 
779                     for (int k = 0; k < lastDecileIndex; k++) {
780                         if (value < binUpperBounds[k]) {
781                             ++observed[k];
782                             continue SAMPLE;
783                         }
784                     }
785                     if (value >= bound) {
786                         Assertions.fail(String.format("Sample %d not within bound [%d, %d)",
787                                                       value, origin, bound));
788                     }
789                     ++observed[lastDecileIndex];
790                 }
791 
792                 // Compute chi-square.
793                 double chi2 = 0;
794                 for (int k = 0; k < numBins; k++) {
795                     final double diff = observed[k] - expected[k];
796                     chi2 += diff * diff / expected[k];
797                 }
798 
799                 // Statistics check.
800                 if (chi2 > chi2CriticalValue) {
801                     failedStat.add(chi2);
802                 }
803             }
804         } catch (final Exception e) {
805             // Should never happen.
806             throw new RuntimeException("Unexpected", e);
807         }
808 
809         // The expected number of failed tests can be modelled as a Binomial distribution
810         // B(n, p) with n=500, p=0.01 (500 tests with a 1% significance level).
811         // The cumulative probability of the number of failed tests (X) is:
812         // x     P(X>x)
813         // 10    0.0132
814         // 11    0.00521
815         // 12    0.00190
816 
817         if (failedStat.size() > 11) { // Test will fail with 0.5% probability
818             Assertions.fail(String.format(
819                 "%s: Too many failures for n = %d, sample size = %d " +
820                 "(%d out of %d tests failed, chi2 > %.3f=%s)",
821                 method, bound, SAMPLE_SIZE, failedStat.size(), numTests, chi2CriticalValue,
822                 failedStat.stream().map(d -> String.format("%.3f", d))
823                           .collect(Collectors.joining(", ", "[", "]"))));
824         }
825     }
826 }