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.internal;
18  
19  import java.util.Map;
20  import java.util.Arrays;
21  import java.util.HashMap;
22  import org.junit.jupiter.api.Assertions;
23  import org.junit.jupiter.api.Test;
24  import org.junit.jupiter.params.ParameterizedTest;
25  import org.junit.jupiter.params.provider.CsvSource;
26  import org.junit.jupiter.params.provider.ValueSource;
27  import org.apache.commons.rng.UniformRandomProvider;
28  import org.apache.commons.rng.core.source32.IntProvider;
29  import org.apache.commons.rng.core.source64.LongProvider;
30  import org.apache.commons.rng.core.source64.RandomLongSource;
31  import org.apache.commons.rng.core.source64.SplitMix64;
32  import org.apache.commons.rng.core.util.NumberFactory;
33  
34  /**
35   * Tests for {@link SeedFactory}.
36   */
37  class SeedFactoryTest {
38      @Test
39      void testCreateLong() {
40          final Map<Long, Integer> values = new HashMap<>();
41  
42          final int n = 100000;
43          for (int i = 0; i < n; i++) {
44              final long v = SeedFactory.createLong();
45  
46              Integer count = values.get(v);
47              if (count == null) {
48                  count = 0;
49              }
50              values.put(v, count + 1);
51          }
52  
53          // Check that all seeds are different.
54          assertDifferentValues(values);
55      }
56  
57      @Test
58      void testCreateLongArray() {
59          final Map<Long, Integer> values = new HashMap<>();
60  
61          final int n = 100000;
62          final long[] array = SeedFactory.createLongArray(n);
63          Assertions.assertEquals(n, array.length);
64  
65          for (long v : array) {
66              Integer count = values.get(v);
67              if (count == null) {
68                  count = 0;
69              }
70              values.put(v, count + 1);
71          }
72  
73          // Check that all seeds are different.
74          assertDifferentValues(values);
75      }
76  
77      @Test
78      void testCreateIntArray() {
79          final Map<Long, Integer> values = new HashMap<>();
80  
81          for (int i = 0; i < 50000; i++) {
82              final int[] a = SeedFactory.createIntArray(2);
83              final long v = NumberFactory.makeLong(a[0], a[1]);
84              Integer count = values.get(v);
85              if (count == null) {
86                  count = 0;
87              }
88              values.put(v, count + 1);
89          }
90  
91          // Check that all pairs in are different.
92          assertDifferentValues(values);
93      }
94  
95      /**
96       * Asserts that all the keys in given {@code map} have their
97       * value equal to 1.
98       *
99       * @param map Map to counts.
100      */
101     private static <T> void assertDifferentValues(Map<T, Integer> map) {
102         final StringBuilder sb = new StringBuilder();
103 
104         int duplicates = 0;
105         for (Map.Entry<T, Integer> entry : map.entrySet()) {
106             final int count = entry.getValue();
107             if (count <= 0) {
108                 throw new IllegalStateException();
109             }
110 
111             if (count > 1) {
112                 duplicates += count - 1;
113                 sb.append(entry.getKey() + ": " + count + "\n");
114             }
115         }
116 
117         if (duplicates > 0) {
118             Assertions.fail(duplicates + " duplicates\n" + sb);
119         }
120     }
121 
122     @Test
123     void testCreateIntArrayWithZeroSize() {
124         Assertions.assertArrayEquals(new int[0], SeedFactory.createIntArray(0));
125     }
126 
127     @Test
128     void testCreateIntArrayWithCompleteBlockSize() {
129         // Block size is 8 for int
130         assertCreateIntArray(8);
131     }
132 
133     @Test
134     void testCreateIntArrayWithIncompleteBlockSize() {
135         // Block size is 8 for int
136         assertCreateIntArray(8 + 1);
137     }
138 
139     /**
140      * Checks that the int array values can be placed into 2 bins with
141      * approximately equal number of counts.
142      * The test uses the expectation from a fixed-step "random walk".
143      *
144      * @param n The size of the array.
145      */
146     private static void assertCreateIntArray(int n) {
147         final int[] array = SeedFactory.createIntArray(n);
148         Assertions.assertEquals(n, array.length, "Incorrect array length");
149         // The bit count should be 50%.
150         int bitCount = 0;
151         for (final int i : array) {
152             bitCount += Integer.bitCount(i);
153         }
154         final int numberOfBits = n * Integer.SIZE;
155         assertMonobit(bitCount, numberOfBits);
156     }
157 
158     @Test
159     void testCreateLongArrayWithZeroSize() {
160         Assertions.assertArrayEquals(new long[0], SeedFactory.createLongArray(0));
161     }
162 
163     @Test
164     void testCreateLongArrayWithCompleteBlockSize() {
165         // Block size is 4 for long
166         assertCreateLongArray(4);
167     }
168 
169     @Test
170     void testCreateLongArrayWithIncompleteBlockSize() {
171         // Block size is 4 for long
172         assertCreateLongArray(4 + 1);
173     }
174 
175     /**
176      * Checks that the long array values can be placed into 2 bins with
177      * approximately equal number of counts.
178      * The test uses the expectation from a fixed-step "random walk".
179      *
180      * @param n The size of the array.
181      */
182     private static void assertCreateLongArray(int n) {
183         final long[] array = SeedFactory.createLongArray(n);
184         Assertions.assertEquals(n, array.length, "Incorrect array length");
185         // The bit count should be 50%.
186         int bitCount = 0;
187         for (final long i : array) {
188             bitCount += Long.bitCount(i);
189         }
190         final int numberOfBits = n * Long.SIZE;
191         assertMonobit(bitCount, numberOfBits);
192     }
193 
194     /**
195      * Assert that the number of 1 bits is approximately 50%. This is based upon a
196      * fixed-step "random walk" of +1/-1 from zero.
197      *
198      * <p>The test is equivalent to the NIST Monobit test with a fixed p-value of 0.0001.
199      * The number of bits is recommended to be above 100.</p>
200      *
201      * @see <A
202      * href="https://csrc.nist.gov/publications/detail/sp/800-22/rev-1a/final">Bassham, et
203      * al (2010) NIST SP 800-22: A Statistical Test Suite for Random and Pseudorandom
204      * Number Generators for Cryptographic Applications. Section 2.1.</a>
205      *
206      * @param bitCount The bit count.
207      * @param numberOfBits Number of bits.
208      */
209     private static void assertMonobit(int bitCount, int numberOfBits) {
210         // Convert the bit count into a number of +1/-1 steps.
211         final double sum = 2.0 * bitCount - numberOfBits;
212         // The reference distribution is Normal with a standard deviation of sqrt(n).
213         // Check the absolute position is not too far from the mean of 0 with a fixed
214         // p-value of 0.0001 taken from a 2-tailed Normal distribution. Computation of
215         // the p-value requires the complimentary error function.
216         // The p-value is set to be equal to a 0.01 with 1 allowed re-run.
217         // (Re-runs are not configured for this test.)
218         final double absSum = Math.abs(sum);
219         final double max = Math.sqrt(numberOfBits) * 3.891;
220         Assertions.assertTrue(absSum <= max,
221             () -> "Walked too far astray: " + absSum +
222                   " > " + max +
223                   " (test will fail randomly about 1 in 10,000 times)");
224     }
225 
226     @Test
227     void testCreateByteArrayWithSizeZero() {
228         assertCreateByteArray(new byte[0]);
229     }
230 
231     @Test
232     void testCreateByteArrayIgnoresNonZeroPositions() {
233         final byte position = 123;
234         int n = 3;
235         for (int i = 0; i < n; i++) {
236             final byte[] expected = new byte[n];
237             expected[i] = position;
238             assertCreateByteArray(expected);
239         }
240     }
241 
242     /**
243      * Assert that the SeedFactory uses the bytes exactly as generated by the
244      * {@link UniformRandomProvider#nextBytes(byte[])} method (assuming they are not all zero).
245      *
246      * @param expected the expected
247      */
248     private static void assertCreateByteArray(final byte[] expected) {
249         final UniformRandomProvider rng = new IntProvider() {
250             @Override
251             public int next() {
252                 Assertions.fail("This method should not be used");
253                 return 0;
254             }
255 
256             @Override
257             public void nextBytes(byte[] bytes) {
258                 System.arraycopy(expected, 0, bytes, 0, Math.min(expected.length, bytes.length));
259             }
260         };
261 
262         final byte[] seed = SeedFactory.createByteArray(rng, expected.length, 0, expected.length);
263         Assertions.assertArrayEquals(expected, seed);
264     }
265 
266     @Test
267     void testCreateByteArrayWithAllZeroBytesUpdatesFromTo() {
268         final UniformRandomProvider rng = new IntProvider() {
269             @Override
270             public int next() {
271                 // Deliberately produce zero
272                 return 0;
273             }
274         };
275         // Test the method only replaces the target sub-range.
276         // Test all sub-ranges.
277         final int size = 4;
278         for (int start = 0; start < size; start++) {
279             final int from = start;
280             for (int end = start; end < size; end++) {
281                 final int to = end;
282                 final byte[] seed = SeedFactory.createByteArray(rng, 4, from, to);
283 
284                 // Validate
285                 for (int i = 0; i < from; i++) {
286                     final int index = i;
287                     Assertions.assertEquals(0, seed[i],
288                         () -> String.format("[%d, %d) zero at position %d should not be modified",
289                             from, to, index));
290                 }
291                 if (to > from) {
292                     byte allBits = 0;
293                     for (int i = from; i < to; i++) {
294                         allBits |= seed[i];
295                     }
296                     Assertions.assertNotEquals(0, allBits,
297                         () -> String.format("[%d, %d) should not be all zero", from, to));
298                 }
299                 for (int i = to; i < size; i++) {
300                     final int index = i;
301                     Assertions.assertEquals(0, seed[i],
302                         () -> String.format("[%d, %d) zero at position %d should not be modified",
303                             from, to, index));
304                 }
305             }
306         }
307     }
308 
309     @Test
310     void testEnsureNonZeroIntArrayIgnoresEmptySeed() {
311         final int[] seed = new int[0];
312         SeedFactory.ensureNonZero(seed, 0, 0);
313         // Note: Nothing to assert.
314         // This tests an ArrayIndexOutOfBoundsException does not occur.
315     }
316 
317     @ParameterizedTest
318     @CsvSource({
319         "0, 0, 1",
320         "1, -1, 1",
321         "1, 0, 2",
322         "1, 1, 2",
323     })
324     void testEnsureNonZeroIntArrayThrowsWithInvalidRange(int n, int from, int to) {
325         final int[] seed = new int[n];
326         Assertions.assertThrows(IndexOutOfBoundsException.class,
327             () -> SeedFactory.ensureNonZero(seed, from, to));
328     }
329 
330 
331     @Test
332     void testEnsureNonZeroIntArrayWithAllZeroBytesUpdatesFromTo() {
333         // Test the method only replaces the target sub-range.
334         // Test all sub-ranges.
335         final int size = 4;
336         final int[] seed = new int[size];
337         for (int start = 0; start < size; start++) {
338             final int from = start;
339             for (int end = start; end < size; end++) {
340                 final int to = end;
341                 Arrays.fill(seed, 0);
342                 SeedFactory.ensureNonZero(seed, from, to);
343 
344                 // Validate
345                 for (int i = 0; i < from; i++) {
346                     final int index = i;
347                     Assertions.assertEquals(0, seed[i],
348                         () -> String.format("[%d, %d) zero at position %d should not be modified",
349                             from, to, index));
350                 }
351                 if (to > from) {
352                     int allBits = 0;
353                     for (int i = from; i < to; i++) {
354                         allBits |= seed[i];
355                     }
356                     Assertions.assertNotEquals(0, allBits,
357                         () -> String.format("[%d, %d) should not be all zero", from, to));
358                 }
359                 for (int i = to; i < size; i++) {
360                     final int index = i;
361                     Assertions.assertEquals(0, seed[i],
362                         () -> String.format("[%d, %d) zero at position %d should not be modified",
363                             from, to, index));
364                 }
365             }
366         }
367     }
368 
369     @Test
370     void testEnsureNonZeroLongArrayIgnoresEmptySeed() {
371         final long[] seed = new long[0];
372         SeedFactory.ensureNonZero(seed, 0, 0);
373         // Note: Nothing to assert.
374         // This tests an ArrayIndexOutOfBoundsException does not occur.
375     }
376 
377     @ParameterizedTest
378     @CsvSource({
379         "0, 0, 1",
380         "1, -1, 1",
381         "1, 0, 2",
382         "1, 1, 2",
383     })
384     void testEnsureNonZeroLongArrayThrowsWithInvalidRange(int n, int from, int to) {
385         final long[] seed = new long[n];
386         Assertions.assertThrows(IndexOutOfBoundsException.class,
387             () -> SeedFactory.ensureNonZero(seed, from, to));
388     }
389 
390 
391     @Test
392     void testEnsureNonZeroLongArrayWithAllZeroBytesUpdatesFromTo() {
393         // Test the method only replaces the target sub-range.
394         // Test all sub-ranges.
395         final int size = 4;
396         final long[] seed = new long[size];
397         for (int start = 0; start < size; start++) {
398             final int from = start;
399             for (int end = start; end < size; end++) {
400                 final int to = end;
401                 Arrays.fill(seed, 0);
402                 SeedFactory.ensureNonZero(seed, from, to);
403 
404                 // Validate
405                 for (int i = 0; i < from; i++) {
406                     final int index = i;
407                     Assertions.assertEquals(0, seed[i],
408                         () -> String.format("[%d, %d) zero at position %d should not be modified",
409                             from, to, index));
410                 }
411                 if (to > from) {
412                     long allBits = 0;
413                     for (int i = from; i < to; i++) {
414                         allBits |= seed[i];
415                     }
416                     Assertions.assertNotEquals(0, allBits,
417                         () -> String.format("[%d, %d) should not be all zero", from, to));
418                 }
419                 for (int i = to; i < size; i++) {
420                     final int index = i;
421                     Assertions.assertEquals(0, seed[i],
422                         () -> String.format("[%d, %d) zero at position %d should not be modified",
423                             from, to, index));
424                 }
425             }
426         }
427     }
428 
429     @Test
430     void testEnsureNonZeroByteArrayIgnoresEmptySeed() {
431         final byte[] seed = new byte[0];
432         final UniformRandomProvider rng = new SplitMix64(123);
433         SeedFactory.ensureNonZero(seed, 0, 0, rng);
434         // Note: Nothing to assert.
435         // This tests an ArrayIndexOutOfBoundsException does not occur.
436     }
437 
438     @ParameterizedTest
439     @CsvSource({
440         "0, 0, 1",
441         "1, -1, 1",
442         "1, 0, 2",
443         "1, 1, 2",
444     })
445     void testEnsureNonZeroByteArrayThrowsWithInvalidRange(int n, int from, int to) {
446         final byte[] seed = new byte[n];
447         final UniformRandomProvider rng = new SplitMix64(123);
448         Assertions.assertThrows(IndexOutOfBoundsException.class,
449             () -> SeedFactory.ensureNonZero(seed, from, to, rng));
450     }
451 
452     /**
453      * Test the fixed values returned for the zero byte array source of randomness
454      * have specific properties.
455      */
456     @Test
457     void testZeroByteArraySourceOfRandomness() {
458         Assertions.assertEquals(0, MixFunctions.stafford13(-MixFunctions.GOLDEN_RATIO_64 + MixFunctions.GOLDEN_RATIO_64));
459         Assertions.assertEquals(0, MixFunctions.stafford13((1463436497261722119L << 1) + MixFunctions.GOLDEN_RATIO_64) & (-1L >>> 56));
460         Assertions.assertEquals(0, MixFunctions.stafford13((4949471497809997598L << 1) + MixFunctions.GOLDEN_RATIO_64) & (-1L >>> 48));
461         // Note:
462         // Finding a value x where MixFunctions.stafford13((x << 1) + MixFunctions.GOLDEN_RATIO_64)
463         // is zero for the least significant 7 bytes used inversion of the mix function.
464         // See the MixFunctionsTest for a routine to perform the unmixing.
465         Assertions.assertEquals(0, MixFunctions.stafford13((953042962641938212L << 1) + MixFunctions.GOLDEN_RATIO_64) & (-1L >>> 8));
466     }
467 
468     @ParameterizedTest
469     @ValueSource(longs = {0, -MixFunctions.GOLDEN_RATIO_64, 1463436497261722119L, 4949471497809997598L, 953042962641938212L})
470     void testEnsureNonZeroByteArrayWithAllZeroBytesUpdatesFromTo(long next) {
471         // Use a fixed source of randomness to demonstrate the method is robust.
472         // This should only be used when the sub-range is non-zero length.
473         int[] calls = {0};
474         final UniformRandomProvider rng = new LongProvider() {
475             @Override
476             public long next() {
477                 calls[0]++;
478                 return next;
479             }
480         };
481 
482         // Test the method only replaces the target sub-range.
483         // Test all sub-ranges up to size.
484         final int size = 2 * Long.BYTES;
485         final byte[] seed = new byte[size];
486         for (int start = 0; start < size; start++) {
487             final int from = start;
488             for (int end = start; end < size; end++) {
489                 final int to = end;
490                 Arrays.fill(seed, (byte) 0);
491                 final int before = calls[0];
492                 SeedFactory.ensureNonZero(seed, from, to, rng);
493 
494                 // Validate
495                 for (int i = 0; i < from; i++) {
496                     final int index = i;
497                     Assertions.assertEquals(0, seed[i],
498                         () -> String.format("[%d, %d) zero at position %d should not be modified",
499                             from, to, index));
500                 }
501                 if (to > from) {
502                     byte allBits = 0;
503                     for (int i = from; i < to; i++) {
504                         allBits |= seed[i];
505                     }
506                     Assertions.assertNotEquals(0, allBits,
507                         () -> String.format("[%d, %d) should not be all zero", from, to));
508                     // Check the source was used to seed the sequence
509                     Assertions.assertNotEquals(before, calls[0],
510                         () -> String.format("[%d, %d) should use the random source", from, to));
511                 } else {
512                     // Check the source was not used
513                     Assertions.assertEquals(before, calls[0],
514                         () -> String.format("[%d, %d) should not use the random source", from, to));
515                 }
516                 for (int i = to; i < size; i++) {
517                     final int index = i;
518                     Assertions.assertEquals(0, seed[i],
519                         () -> String.format("[%d, %d) zero at position %d should not be modified",
520                             from, to, index));
521                 }
522             }
523         }
524     }
525 
526     @Test
527     void testEnsureNonZeroValue() {
528         final long expected = 345;
529         RandomLongSource source = new RandomLongSource() {
530             @Override
531             public long next() {
532                 return expected;
533             }
534         };
535         Assertions.assertEquals(expected, SeedFactory.ensureNonZero(source, 0),
536             "Zero should be replaced using the random source");
537         for (final long nonZero : new long[] {Long.MIN_VALUE, -1, 1, 9876654321L, Long.MAX_VALUE}) {
538             Assertions.assertEquals(nonZero, SeedFactory.ensureNonZero(source, nonZero),
539                 "Non-zero should be unmodified");
540         }
541     }
542 }