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.simple;
18  
19  import java.util.Arrays;
20  import java.util.List;
21  import java.util.ArrayList;
22  import java.util.function.LongSupplier;
23  import java.util.stream.Collectors;
24  import java.io.IOException;
25  import java.io.ObjectOutputStream;
26  import java.io.ObjectInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.ByteArrayInputStream;
29  
30  import org.junit.jupiter.api.Assertions;
31  import org.junit.jupiter.api.Assumptions;
32  import org.junit.jupiter.params.ParameterizedTest;
33  import org.junit.jupiter.params.provider.MethodSource;
34  
35  import org.apache.commons.rng.UniformRandomProvider;
36  import org.apache.commons.rng.JumpableUniformRandomProvider;
37  import org.apache.commons.rng.LongJumpableUniformRandomProvider;
38  import org.apache.commons.rng.RandomProviderState;
39  import org.apache.commons.rng.RestorableUniformRandomProvider;
40  import org.apache.commons.rng.SplittableUniformRandomProvider;
41  import org.apache.commons.rng.core.RandomProviderDefaultState;
42  import org.apache.commons.rng.core.source64.LongProvider;
43  import org.apache.commons.rng.core.source64.SplitMix64;
44  
45  /**
46   * Tests which all generators must pass.
47   */
48  class ProvidersCommonParametricTest {
49      private static Iterable<ProvidersList.Data> getProvidersTestData() {
50          return ProvidersList.list();
51      }
52  
53      // Seeding tests.
54  
55      @ParameterizedTest
56      @MethodSource("getProvidersTestData")
57      void testUnsupportedSeedType(ProvidersList.Data data) {
58          final RandomSource originalSource = data.getSource();
59          final byte seed = 123;
60          final Object[] originalArgs = data.getArgs();
61          Assertions.assertThrows(UnsupportedOperationException.class,
62              () -> originalSource.create(seed, originalArgs));
63      }
64  
65      /**
66       * Test the factory create method returns the same class as the instance create method.
67       */
68      @ParameterizedTest
69      @MethodSource("getProvidersTestData")
70      void testFactoryCreateMethod(ProvidersList.Data data) {
71          final RandomSource originalSource = data.getSource();
72          final Object originalSeed = data.getSeed();
73          final Object[] originalArgs = data.getArgs();
74          // Cannot test providers that require arguments
75          Assumptions.assumeTrue(originalArgs == null);
76          @SuppressWarnings("deprecation")
77          final UniformRandomProvider rng = RandomSource.create(data.getSource());
78          final UniformRandomProvider generator = originalSource.create(originalSeed, originalArgs);
79          Assertions.assertEquals(generator.getClass(), rng.getClass());
80      }
81  
82      /**
83       * Test the factory create method returns the same class as the instance create method
84       * and produces the same output.
85       */
86      @ParameterizedTest
87      @MethodSource("getProvidersTestData")
88      void testFactoryCreateMethodWithSeed(ProvidersList.Data data) {
89          final RandomSource originalSource = data.getSource();
90          final Object originalSeed = data.getSeed();
91          final Object[] originalArgs = data.getArgs();
92          final UniformRandomProvider generator = originalSource.create(originalSeed, originalArgs);
93          @SuppressWarnings("deprecation")
94          final UniformRandomProvider rng1 = RandomSource.create(originalSource, originalSeed, originalArgs);
95          Assertions.assertEquals(rng1.getClass(), generator.getClass());
96          // Check the output
97          final UniformRandomProvider rng2 = originalSource.create(originalSeed, originalArgs);
98          for (int i = 0; i < 10; i++) {
99              Assertions.assertEquals(rng2.nextLong(), rng1.nextLong());
100         }
101     }
102 
103     /**
104      * Test the create method throws an {@link IllegalArgumentException} if passed the wrong
105      * arguments.
106      */
107     @ParameterizedTest
108     @MethodSource("getProvidersTestData")
109     void testCreateMethodThrowsWithIncorrectArguments(ProvidersList.Data data) {
110         final RandomSource originalSource = data.getSource();
111         final Object[] originalArgs = data.getArgs();
112         if (originalArgs == null) {
113             // Try passing arguments to a provider that does not require them
114             final int arg1 = 123;
115             final double arg2 = 456.0;
116             Assertions.assertThrows(IllegalArgumentException.class,
117                 () -> originalSource.create(arg1, arg2),
118                 () -> "Source does not require arguments: " + originalSource);
119         } else {
120             // Try no arguments for a provider that does require them
121             Assertions.assertThrows(IllegalArgumentException.class,
122                 () -> originalSource.create(),
123                 () -> "Source requires arguments: " + originalSource);
124         }
125     }
126 
127     @ParameterizedTest
128     @MethodSource("getProvidersTestData")
129     void testAllSeedTypes(ProvidersList.Data data) {
130         final RandomSource originalSource = data.getSource();
131         final Object originalSeed = data.getSeed();
132         final Object[] originalArgs = data.getArgs();
133         final Integer intSeed = -12131415;
134         final Long longSeed = -1213141516171819L;
135         final int[] intArraySeed = new int[] {0, 11, -22, 33, -44, 55, -66, 77, -88, 99};
136         final long[] longArraySeed = new long[] {11111L, -222222L, 3333333L, -44444444L};
137         final byte[] byteArraySeed = new byte[] {-128, -91, -45, -32, -1, 0, 11, 23, 54, 88, 127};
138 
139         final Object[] seeds = new Object[] {null,
140                                              intSeed,
141                                              longSeed,
142                                              intArraySeed,
143                                              longArraySeed,
144                                              byteArraySeed};
145 
146         int nonNativeSeedCount = 0;
147         int seedCount = 0;
148         for (final Object s : seeds) {
149             ++seedCount;
150             if (originalSource.isNativeSeed(s)) {
151                 Assertions.assertNotNull(s, "Identified native seed is null");
152                 Assertions.assertEquals(s.getClass(), originalSeed.getClass(),
153                     "Incorrect identification of native seed type");
154             } else {
155                 ++nonNativeSeedCount;
156             }
157 
158             originalSource.create(s, originalArgs);
159         }
160 
161         Assertions.assertEquals(6, seedCount);
162         Assertions.assertEquals(5, nonNativeSeedCount);
163     }
164 
165     @ParameterizedTest
166     @MethodSource("getProvidersTestData")
167     void testNullSeed(ProvidersList.Data data) {
168         final RandomSource originalSource = data.getSource();
169         final Object[] originalArgs = data.getArgs();
170         // Note: This is the only test that explicitly calls RandomSource.create() with no other arguments.
171         final UniformRandomProvider rng = originalArgs == null ?
172             originalSource.create() :
173             originalSource.create(null, originalArgs);
174         checkNextIntegerInRange(rng, 10, 10000);
175     }
176 
177     @ParameterizedTest
178     @MethodSource("getProvidersTestData")
179     void testEmptyIntArraySeed(ProvidersList.Data data) {
180         final RandomSource originalSource = data.getSource();
181         final Object[] originalArgs = data.getArgs();
182         final int[] empty = new int[0];
183         Assumptions.assumeTrue(originalSource.isNativeSeed(empty));
184 
185         // Exercise the default seeding procedure.
186         final UniformRandomProvider rng = originalSource.create(empty, originalArgs);
187         checkNextIntegerInRange(rng, 10, 20000);
188     }
189 
190     @ParameterizedTest
191     @MethodSource("getProvidersTestData")
192     void testEmptyLongArraySeed(ProvidersList.Data data) {
193         final RandomSource originalSource = data.getSource();
194         final Object[] originalArgs = data.getArgs();
195         final long[] empty = new long[0];
196         Assumptions.assumeTrue(originalSource.isNativeSeed(empty));
197 
198         // Exercise the default seeding procedure.
199         final UniformRandomProvider rng = originalSource.create(empty, originalArgs);
200         checkNextIntegerInRange(rng, 10, 10000);
201     }
202 
203     @ParameterizedTest
204     @MethodSource("getProvidersTestData")
205     void testZeroIntArraySeed(ProvidersList.Data data) {
206         final RandomSource originalSource = data.getSource();
207         final Object[] originalArgs = data.getArgs();
208         // Exercise capacity to escape all "zero" state.
209         final int[] zero = new int[2000]; // Large enough to fill the entire state with zeroes.
210         final UniformRandomProvider rng = originalSource.create(zero, originalArgs);
211         Assumptions.assumeTrue(createsNonZeroLongOutput(rng, 2000),
212             () -> "RNG is non-functional with an all zero seed: " + originalSource);
213         checkNextIntegerInRange(rng, 10, 10000);
214     }
215 
216     @ParameterizedTest
217     @MethodSource("getProvidersTestData")
218     void testZeroLongArraySeed(ProvidersList.Data data) {
219         final RandomSource originalSource = data.getSource();
220         final Object[] originalArgs = data.getArgs();
221         // Exercise capacity to escape all "zero" state.
222         final long[] zero = new long[2000]; // Large enough to fill the entire state with zeroes.
223         final UniformRandomProvider rng = originalSource.create(zero, originalArgs);
224         Assumptions.assumeTrue(createsNonZeroLongOutput(rng, 2000),
225             () -> "RNG is non-functional with an all zero seed: " + originalSource);
226         checkNextIntegerInRange(rng, 10, 10000);
227     }
228 
229     @ParameterizedTest
230     @MethodSource("getProvidersTestData")
231     void testRandomSourceCreateSeed(ProvidersList.Data data) {
232         final RandomSource originalSource = data.getSource();
233         final Object[] originalArgs = data.getArgs();
234         final byte[] seed = originalSource.createSeed();
235         final UniformRandomProvider rng = originalSource.create(seed, originalArgs);
236         checkNextIntegerInRange(rng, 10, 10000);
237     }
238 
239     @ParameterizedTest
240     @MethodSource("getProvidersTestData")
241     void testRandomSourceCreateSeedFromRNG(ProvidersList.Data data) {
242         final RandomSource originalSource = data.getSource();
243         final Object[] originalArgs = data.getArgs();
244         final byte[] seed = originalSource.createSeed(new SplitMix64(RandomSource.createLong()));
245         final UniformRandomProvider rng = originalSource.create(seed, originalArgs);
246         checkNextIntegerInRange(rng, 10, 10000);
247     }
248 
249     @ParameterizedTest
250     @MethodSource("getProvidersTestData")
251     void testRandomSourceCreateSeedFromInvalidRNG(ProvidersList.Data data) {
252         // Create a RNG that will fill a byte[] seed with all zeros.
253         // The ensure non-zero range checks in the RandomSource should
254         // create a good seed and a functional RNG.
255         final LongProvider badRng = new LongProvider() {
256             @Override
257             public long next() {
258                 return 0;
259             }
260         };
261 
262         final RandomSource originalSource = data.getSource();
263         final byte[] seed = originalSource.createSeed(badRng);
264 
265         // The seed generation should be repeatable
266         Assertions.assertArrayEquals(seed, originalSource.createSeed(badRng));
267 
268         // The RNG seed will create a functional generator
269         final Object[] originalArgs = data.getArgs();
270         final UniformRandomProvider rng = originalSource.create(seed, originalArgs);
271         checkNextIntegerInRange(rng, 10, 10000);
272     }
273 
274     // State save and restore tests.
275 
276     @SuppressWarnings("unused")
277     @ParameterizedTest
278     @MethodSource("getProvidersTestData")
279     void testUnrestorable(ProvidersList.Data data) {
280         final RandomSource originalSource = data.getSource();
281         final Object originalSeed = data.getSeed();
282         final Object[] originalArgs = data.getArgs();
283         // Create two generators of the same type as the one being tested.
284         final UniformRandomProvider rng1 = originalSource.create(originalSeed, originalArgs);
285         final UniformRandomProvider rng2 = RandomSource.unrestorable(originalSource.create(originalSeed, originalArgs));
286 
287         // Ensure that they generate the same values.
288         RandomAssert.assertProduceSameSequence(rng1, rng2);
289 
290         // Cast must work.
291         final RestorableUniformRandomProvider restorable = (RestorableUniformRandomProvider) rng1;
292         // Cast must fail.
293         Assertions.assertThrows(ClassCastException.class, () -> {
294             final RestorableUniformRandomProvider dummy = (RestorableUniformRandomProvider) rng2;
295         });
296     }
297 
298     @ParameterizedTest
299     @MethodSource("getProvidersTestData")
300     void testSerializingState(ProvidersList.Data data)
301         throws IOException,
302                ClassNotFoundException {
303         final UniformRandomProvider generator = data.getSource().create(data.getSeed(), data.getArgs());
304 
305         // Large "n" is not necessary here as we only test the serialization.
306         final int n = 100;
307 
308         // Cast is OK: all instances created by this library inherit from "BaseProvider".
309         final RestorableUniformRandomProvider restorable = (RestorableUniformRandomProvider) generator;
310 
311         // Save.
312         final RandomProviderState stateOrig = restorable.saveState();
313         // Serialize.
314         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
315         final ObjectOutputStream oos = new ObjectOutputStream(bos);
316         oos.writeObject(((RandomProviderDefaultState) stateOrig).getState());
317 
318         // Store some values.
319         final List<Number> listOrig = makeList(n, generator);
320 
321         // Discard a few more.
322         final List<Number> listDiscard = makeList(n, generator);
323         Assertions.assertNotEquals(0, listDiscard.size());
324         Assertions.assertNotEquals(listOrig, listDiscard);
325 
326         // Retrieve from serialized stream.
327         final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
328         final ObjectInputStream ois = new ObjectInputStream(bis);
329         final RandomProviderState stateNew = new RandomProviderDefaultState((byte[]) ois.readObject());
330 
331         Assertions.assertNotSame(stateOrig, stateNew);
332 
333         // Reset.
334         restorable.restoreState(stateNew);
335 
336         // Replay.
337         final List<Number> listReplay = makeList(n, generator);
338         Assertions.assertNotSame(listOrig, listReplay);
339 
340         // Check that the serialized data recreated the orginal state.
341         Assertions.assertEquals(listOrig, listReplay);
342     }
343 
344     @ParameterizedTest
345     @MethodSource("getProvidersTestData")
346     void testUnrestorableToString(ProvidersList.Data data) {
347         final UniformRandomProvider generator = data.getSource().create(data.getSeed(), data.getArgs());
348         Assertions.assertEquals(generator.toString(),
349                                 RandomSource.unrestorable(generator).toString());
350     }
351 
352     @ParameterizedTest
353     @MethodSource("getProvidersTestData")
354     void testSupportedInterfaces(ProvidersList.Data data) {
355         final RandomSource originalSource = data.getSource();
356         final Object[] originalArgs = data.getArgs();
357         final UniformRandomProvider rng = originalSource.create(null, originalArgs);
358         Assertions.assertEquals(rng instanceof JumpableUniformRandomProvider,
359                                 originalSource.isJumpable(),
360                                 "isJumpable");
361         Assertions.assertEquals(rng instanceof LongJumpableUniformRandomProvider,
362                                 originalSource.isLongJumpable(),
363                                 "isLongJumpable");
364         Assertions.assertEquals(rng instanceof SplittableUniformRandomProvider,
365                                 originalSource.isSplittable(),
366                                 "isSplittable");
367     }
368 
369     ///// Support methods below.
370 
371 
372     // The methods
373     //   * makeList
374     //   * checkNextIntegerInRange
375     //   * checkNextInRange
376     // have been copied from "src/test" in module "commons-rng-core".
377     // TODO: check whether it is possible to have a single implementation.
378 
379     /**
380      * Populates a list with random numbers.
381      *
382      * @param n Loop counter.
383      * @param generator Random generator.
384      * @return a list containing {@code 11 * n} random numbers.
385      */
386     private static List<Number> makeList(int n, UniformRandomProvider generator) {
387         final List<Number> list = new ArrayList<>();
388 
389         for (int i = 0; i < n; i++) {
390             // Append 11 values.
391             list.add(generator.nextInt());
392             list.add(generator.nextInt(21));
393             list.add(generator.nextInt(436));
394             list.add(generator.nextLong());
395             list.add(generator.nextLong(157894));
396             list.add(generator.nextLong(5745833));
397             list.add(generator.nextFloat());
398             list.add(generator.nextFloat());
399             list.add(generator.nextDouble());
400             list.add(generator.nextDouble());
401             list.add(generator.nextBoolean() ? 1 : 0);
402         }
403 
404         return list;
405     }
406 
407     /**
408      * Tests uniformity of the distribution produced by {@code nextInt(int)}.
409      *
410      * @param rng Generator.
411      * @param max Upper bound.
412      * @param sampleSize Number of random values generated.
413      * @param generator Random generator.
414      */
415     private static void checkNextIntegerInRange(final UniformRandomProvider rng,
416                                                 final int max,
417                                                 int sampleSize) {
418         final LongSupplier nextMethod = () -> rng.nextInt(max);
419 
420         checkNextInRange(rng, max, sampleSize, nextMethod);
421     }
422 
423     /**
424      * Tests uniformity of the distribution produced by the given
425      * {@code nextMethod}.
426      * It performs a chi-square test of homogeneity of the observed
427      * distribution with the expected uniform distribution.
428      * Repeat tests are performed at the 1% level and the total number of failed
429      * tests is tested at the 0.5% significance level.
430      *
431      * @param n Upper bound.
432      * @param nextMethod method to call.
433      * @param sampleSize Number of random values generated.
434      * @param generator Random generator.
435      */
436     private static void checkNextInRange(UniformRandomProvider generator,
437                                          long n,
438                                          int sampleSize,
439                                          LongSupplier nextMethod) {
440         final int numTests = 500;
441 
442         // Do not change (statistical test assumes that dof = 9).
443         final int numBins = 10; // dof = numBins - 1
444 
445         // Set up bins.
446         final long[] binUpperBounds = new long[numBins];
447         final double step = n / (double) numBins;
448         for (int k = 0; k < numBins; k++) {
449             binUpperBounds[k] = (long) ((k + 1) * step);
450         }
451         // Rounding error occurs on the long value of 2305843009213693951L
452         binUpperBounds[numBins - 1] = n;
453 
454         // Run the tests.
455         int numFailures = 0;
456 
457         final double[] expected = new double[numBins];
458         long previousUpperBound = 0;
459         for (int k = 0; k < numBins; k++) {
460             final long range = binUpperBounds[k] - previousUpperBound;
461             expected[k] = sampleSize * (range / (double) n);
462             previousUpperBound = binUpperBounds[k];
463         }
464 
465         final int[] observed = new int[numBins];
466         // Chi-square critical value with 9 degrees of freedom
467         // and 1% significance level.
468         final double chi2CriticalValue = 21.665994333461924;
469 
470         // For storing chi2 larger than the critical value.
471         final List<Double> failedStat = new ArrayList<>();
472         try {
473             final int lastDecileIndex = numBins - 1;
474             for (int i = 0; i < numTests; i++) {
475                 Arrays.fill(observed, 0);
476                 SAMPLE: for (int j = 0; j < sampleSize; j++) {
477                     final long value = nextMethod.getAsLong();
478                     Assertions.assertTrue(value >= 0 && value < n, "Range");
479 
480                     for (int k = 0; k < lastDecileIndex; k++) {
481                         if (value < binUpperBounds[k]) {
482                             ++observed[k];
483                             continue SAMPLE;
484                         }
485                     }
486                     ++observed[lastDecileIndex];
487                 }
488 
489                 // Compute chi-square.
490                 double chi2 = 0;
491                 for (int k = 0; k < numBins; k++) {
492                     final double diff = observed[k] - expected[k];
493                     chi2 += diff * diff / expected[k];
494                 }
495 
496                 // Statistics check.
497                 if (chi2 > chi2CriticalValue) {
498                     failedStat.add(chi2);
499                     ++numFailures;
500                 }
501             }
502         } catch (final Exception e) {
503             // Should never happen.
504             throw new RuntimeException("Unexpected", e);
505         }
506 
507         // The expected number of failed tests can be modelled as a Binomial distribution
508         // B(n, p) with n=500, p=0.01 (500 tests with a 1% significance level).
509         // The cumulative probability of the number of failed tests (X) is:
510         // x     P(X>x)
511         // 10    0.0132
512         // 11    0.00521
513         // 12    0.00190
514 
515         if (numFailures > 11) { // Test will fail with 0.5% probability
516             Assertions.fail(String.format(
517                     "%s: Too many failures for n = %d, sample size = %d " +
518                     "(%d out of %d tests failed, chi2 > %.3f=%s)",
519                     generator, n, sampleSize, numFailures, numTests, chi2CriticalValue,
520                     failedStat.stream().map(d -> String.format("%.3f", d))
521                               .collect(Collectors.joining(", ", "[", "]"))));
522         }
523     }
524 
525     /**
526      * Return true if the generator creates non-zero output from
527      * {@link UniformRandomProvider#nextLong()} within the given number of cycles.
528      *
529      * @param rng Random generator.
530      * @param cycles Number of cycles.
531      * @return true if non-zero output
532      */
533     private static boolean createsNonZeroLongOutput(UniformRandomProvider rng,
534                                                     int cycles) {
535         boolean nonZero = false;
536         for (int i = 0; i < cycles; i++) {
537             if (rng.nextLong() != 0) {
538                 nonZero = true;
539             }
540         }
541         return nonZero;
542     }
543 }