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.core;
18  
19  import java.util.HashSet;
20  import java.util.Set;
21  import java.util.Spliterator;
22  import java.util.SplittableRandom;
23  import java.util.concurrent.ConcurrentHashMap;
24  import java.util.concurrent.ThreadLocalRandom;
25  import java.util.function.Consumer;
26  import java.util.function.UnaryOperator;
27  import java.util.stream.Stream;
28  import org.apache.commons.rng.SplittableUniformRandomProvider;
29  import org.apache.commons.rng.UniformRandomProvider;
30  import org.apache.commons.rng.core.util.RandomStreamsTestHelper;
31  import org.junit.jupiter.api.Assertions;
32  import org.junit.jupiter.params.ParameterizedTest;
33  import org.junit.jupiter.params.provider.MethodSource;
34  
35  /**
36   * Tests which all {@link SplittableUniformRandomProvider} generators must pass.
37   */
38  class SplittableProvidersParametricTest {
39      /** The expected characteristics for the spliterator from the splittable stream. */
40      private static final int SPLITERATOR_CHARACTERISTICS =
41          Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE;
42  
43      /**
44       * Dummy class for checking the behavior of the SplittableUniformRandomProvider.
45       * All generation and split methods throw an exception. This can be used to test
46       * exception conditions for arguments to default stream functions.
47       */
48      private static class DummyGenerator implements SplittableUniformRandomProvider {
49          /** An instance. */
50          static final DummyGenerator INSTANCE = new DummyGenerator();
51  
52          @Override
53          public long nextLong() {
54              throw new UnsupportedOperationException("The nextLong method should not be invoked");
55          }
56  
57          @Override
58          public SplittableUniformRandomProvider split(UniformRandomProvider source) {
59              throw new UnsupportedOperationException("The split(source) method should not be invoked");
60          }
61      }
62  
63      /**
64       * Thread-safe class for checking the behavior of the SplittableUniformRandomProvider.
65       * Generation methods default to ThreadLocalRandom. Split methods return the same instance.
66       * This is a functioning generator that can be used as a source to seed splitting.
67       */
68      private static class ThreadLocalGenerator implements SplittableUniformRandomProvider {
69          /** An instance. */
70          static final ThreadLocalGenerator INSTANCE = new ThreadLocalGenerator();
71  
72          @Override
73          public long nextLong() {
74              return ThreadLocalRandom.current().nextLong();
75          }
76  
77          @Override
78          public SplittableUniformRandomProvider split(UniformRandomProvider source) {
79              return this;
80          }
81      }
82  
83      /**
84       * Gets the list of splittable generators.
85       *
86       * @return the list
87       */
88      private static Iterable<SplittableUniformRandomProvider> getSplittableProviders() {
89          return ProvidersList.listSplittable();
90      }
91  
92      /**
93       * Test that the split methods throw when the source of randomness is null.
94       */
95      @ParameterizedTest
96      @MethodSource("getSplittableProviders")
97      void testSplitThrowsWithNullSource(SplittableUniformRandomProvider generator) {
98          Assertions.assertThrows(NullPointerException.class, () -> generator.split(null));
99      }
100 
101     /**
102      * Test that the random generator returned from the split is a new instance of the same class.
103      */
104     @ParameterizedTest
105     @MethodSource("getSplittableProviders")
106     void testSplitReturnsANewInstance(SplittableUniformRandomProvider generator) {
107         assertSplitReturnsANewInstance(SplittableUniformRandomProvider::split, generator);
108     }
109 
110     /**
111      * Test that the random generator returned from the split(source) is a new instance of the same class.
112      */
113     @ParameterizedTest
114     @MethodSource("getSplittableProviders")
115     void testSplitWithSourceReturnsANewInstance(SplittableUniformRandomProvider generator) {
116         assertSplitReturnsANewInstance(s -> s.split(ThreadLocalGenerator.INSTANCE), generator);
117     }
118 
119     /**
120      * Assert that the random generator returned from the split function is a new instance of the same class.
121      *
122      * @param splitFunction Split function to test.
123      * @param generator RNG under test.
124      */
125     private static void assertSplitReturnsANewInstance(UnaryOperator<SplittableUniformRandomProvider> splitFunction,
126                                                        SplittableUniformRandomProvider generator) {
127         final UniformRandomProvider child = splitFunction.apply(generator);
128         Assertions.assertNotSame(generator, child, "The child instance should be a different object");
129         Assertions.assertEquals(generator.getClass(), child.getClass(), "The child instance should be the same class");
130         RandomAssert.assertNextLongNotEquals(10, generator, child);
131     }
132 
133     /**
134      * Test that the split method is reproducible when used with the same generator source in the
135      * same state.
136      */
137     @ParameterizedTest
138     @MethodSource("getSplittableProviders")
139     void testSplitWithSourceIsReproducible(SplittableUniformRandomProvider generator) {
140         final long seed = ThreadLocalRandom.current().nextLong();
141         final UniformRandomProvider rng1 = generator.split(new SplittableRandom(seed)::nextLong);
142         final UniformRandomProvider rng2 = generator.split(new SplittableRandom(seed)::nextLong);
143         RandomAssert.assertNextLongEquals(10, rng1, rng2);
144     }
145 
146     /**
147      * Test that the other stream splits methods all call the
148      * {@link SplittableUniformRandomProvider#splits(long, SplittableUniformRandomProvider)} method.
149      * This is tested by checking the spliterator is the same.
150      *
151      * <p>This test serves to ensure the default implementations in SplittableUniformRandomProvider
152      * eventually call the same method. The RNG implementation thus only has to override one method.
153      */
154     @ParameterizedTest
155     @MethodSource("getSplittableProviders")
156     void testSplitsMethodsUseSameSpliterator(SplittableUniformRandomProvider generator) {
157         final long size = 10;
158         final Spliterator<SplittableUniformRandomProvider> s = generator.splits(size, generator).spliterator();
159         Assertions.assertEquals(s.getClass(), generator.splits().spliterator().getClass());
160         Assertions.assertEquals(s.getClass(), generator.splits(size).spliterator().getClass());
161         Assertions.assertEquals(s.getClass(), generator.splits(ThreadLocalGenerator.INSTANCE).spliterator().getClass());
162     }
163 
164     @ParameterizedTest
165     @MethodSource("getSplittableProviders")
166     void testSplitsSize(SplittableUniformRandomProvider generator) {
167         for (final long size : new long[] {0, 1, 7, 13}) {
168             Assertions.assertEquals(size, generator.splits(size).count(), "splits");
169             Assertions.assertEquals(size, generator.splits(size, ThreadLocalGenerator.INSTANCE).count(), "splits with source");
170         }
171     }
172 
173     @ParameterizedTest
174     @MethodSource("getSplittableProviders")
175     void testSplits(SplittableUniformRandomProvider generator) {
176         assertSplits(generator, false);
177     }
178 
179     @ParameterizedTest
180     @MethodSource("getSplittableProviders")
181     void testSplitsParallel(SplittableUniformRandomProvider generator) {
182         assertSplits(generator, true);
183     }
184 
185     /**
186      * Test the splits method returns a stream of unique instances. The test uses a
187      * fixed source of randomness such that the only randomness is from the stream
188      * position.
189      *
190      * @param generator Generator
191      * @param parallel true to use a parallel stream
192      */
193     private static void assertSplits(SplittableUniformRandomProvider generator, boolean parallel) {
194         final long size = 13;
195         for (final long seed : new long[] {0, RandomStreamsTestHelper.createSeed(ThreadLocalGenerator.INSTANCE)}) {
196             final SplittableUniformRandomProvider source = new SplittableUniformRandomProvider() {
197                 @Override
198                 public long nextLong() {
199                     return seed;
200                 }
201 
202                 @Override
203                 public SplittableUniformRandomProvider split(UniformRandomProvider source) {
204                     return this;
205                 }
206             };
207             // Test the assumption that the seed will be passed through (lowest bit is set)
208             Assertions.assertEquals(seed | 1, RandomStreamsTestHelper.createSeed(source));
209 
210             Stream<SplittableUniformRandomProvider> stream = generator.splits(size, source);
211             Assertions.assertFalse(stream.isParallel(), "Initial stream should be sequential");
212             if (parallel) {
213                 stream = stream.parallel();
214                 Assertions.assertTrue(stream.isParallel(), "Stream should be parallel");
215             }
216 
217             // Check the instance is a new object of the same type.
218             // These will be hashed using the system identity hash code.
219             final Set<SplittableUniformRandomProvider> observed = ConcurrentHashMap.newKeySet();
220             observed.add(generator);
221             stream.forEach(r -> {
222                 Assertions.assertTrue(observed.add(r), "Instance should be unique");
223                 Assertions.assertEquals(generator.getClass(), r.getClass());
224             });
225             // Note: observed contains the original generator so subtract 1
226             Assertions.assertEquals(size, observed.size() - 1);
227 
228             // Test instances generate different values.
229             // The only randomness is from the stream position.
230             final long[] values = observed.stream().mapToLong(r -> {
231                 // Warm up generator with some cycles.
232                 // E.g. LXM generators return the first value from the initial state.
233                 for (int i = 0; i < 10; i++) {
234                     r.nextLong();
235                 }
236                 return r.nextLong();
237             }).distinct().toArray();
238             // This test is looking for different values.
239             // To avoid the rare case of not all distinct we relax the threshold to
240             // half the generators. This will spot errors where all generators are
241             // the same.
242             Assertions.assertTrue(values.length > size / 2,
243                 () -> "splits did not seed randomness from the stream position. Initial seed = " + seed);
244         }
245     }
246 
247     // Test adapted from stream tests in commons-rng-client-api module
248 
249     /**
250      * Helper method to raise an assertion error inside an action passed to a Spliterator
251      * when the action should not be invoked.
252      *
253      * @see Spliterator#tryAdvance(Consumer)
254      * @see Spliterator#forEachRemaining(Consumer)
255      */
256     private static void failSpliteratorShouldBeEmpty() {
257         Assertions.fail("Spliterator should not have any remaining elements");
258     }
259 
260     @ParameterizedTest
261     @MethodSource("getSplittableProviders")
262     void testSplitsInvalidStreamSizeThrows(SplittableUniformRandomProvider rng) {
263         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.splits(-1), "splits(size)");
264         final SplittableUniformRandomProvider source = DummyGenerator.INSTANCE;
265         Assertions.assertThrows(IllegalArgumentException.class, () -> rng.splits(-1, source), "splits(size, source)");
266     }
267 
268     @ParameterizedTest
269     @MethodSource("getSplittableProviders")
270     void testSplitsUnlimitedStreamSize(SplittableUniformRandomProvider rng) {
271         assertUnlimitedSpliterator(rng.splits().spliterator(), "splits()");
272         final SplittableUniformRandomProvider source = ThreadLocalGenerator.INSTANCE;
273         assertUnlimitedSpliterator(rng.splits(source).spliterator(), "splits(source)");
274     }
275 
276     /**
277      * Assert the spliterator has an unlimited expected size and the characteristics for a sized
278      * immutable stream.
279      *
280      * @param spliterator Spliterator.
281      * @param msg Error message.
282      */
283     private static void assertUnlimitedSpliterator(Spliterator<?> spliterator, String msg) {
284         Assertions.assertEquals(Long.MAX_VALUE, spliterator.estimateSize(), msg);
285         Assertions.assertTrue(spliterator.hasCharacteristics(SPLITERATOR_CHARACTERISTICS),
286             () -> String.format("%s: characteristics = %s, expected %s", msg,
287                 Integer.toBinaryString(spliterator.characteristics()),
288                 Integer.toBinaryString(SPLITERATOR_CHARACTERISTICS)
289             ));
290     }
291 
292     @ParameterizedTest
293     @MethodSource("getSplittableProviders")
294     void testSplitsNullSourceThrows(SplittableUniformRandomProvider rng) {
295         final SplittableUniformRandomProvider source = null;
296         Assertions.assertThrows(NullPointerException.class, () -> rng.splits(source));
297         Assertions.assertThrows(NullPointerException.class, () -> rng.splits(1, source));
298     }
299 
300     @ParameterizedTest
301     @MethodSource("getSplittableProviders")
302     void testSplitsSpliterator(SplittableUniformRandomProvider rng) {
303         // Split a large spliterator into four smaller ones;
304         // each is used to test different functionality
305         final long size = 41;
306         Spliterator<SplittableUniformRandomProvider> s1 = rng.splits(size).spliterator();
307         Assertions.assertEquals(size, s1.estimateSize());
308         final Spliterator<SplittableUniformRandomProvider> s2 = s1.trySplit();
309         final Spliterator<SplittableUniformRandomProvider> s3 = s1.trySplit();
310         final Spliterator<SplittableUniformRandomProvider> s4 = s2.trySplit();
311         Assertions.assertEquals(size, s1.estimateSize() + s2.estimateSize() + s3.estimateSize() + s4.estimateSize());
312 
313         // s1. Test cannot split indefinitely
314         while (s1.estimateSize() > 1) {
315             final long currentSize = s1.estimateSize();
316             final Spliterator<SplittableUniformRandomProvider> other = s1.trySplit();
317             Assertions.assertEquals(currentSize, s1.estimateSize() + other.estimateSize());
318             s1 = other;
319         }
320         Assertions.assertNull(s1.trySplit(), "Cannot split when size <= 1");
321 
322         // Check the instance is a new object of the same type.
323         // These will be hashed using the system identity hash code.
324         final HashSet<SplittableUniformRandomProvider> observed = new HashSet<>();
325         observed.add(rng);
326 
327         final Consumer<SplittableUniformRandomProvider> action = r -> {
328             Assertions.assertTrue(observed.add(r), "Instance should be unique");
329             Assertions.assertEquals(rng.getClass(), r.getClass());
330         };
331 
332         // s2. Test advance
333         for (long newSize = s2.estimateSize(); newSize-- > 0;) {
334             Assertions.assertTrue(s2.tryAdvance(action));
335             Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size estimate");
336         }
337         Assertions.assertFalse(s2.tryAdvance(r -> failSpliteratorShouldBeEmpty()));
338         s2.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
339 
340         // s3. Test forEachRemaining
341         s3.forEachRemaining(action);
342         Assertions.assertEquals(0, s3.estimateSize());
343         s3.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
344 
345         // s4. Test tryAdvance and forEachRemaining when the action throws an exception
346         final IllegalStateException ex = new IllegalStateException();
347         final Consumer<SplittableUniformRandomProvider> badAction = r -> {
348             throw ex;
349         };
350         final long currentSize = s4.estimateSize();
351         Assertions.assertTrue(currentSize > 1, "Spliterator requires more elements to test advance");
352         Assertions.assertSame(ex, Assertions.assertThrows(IllegalStateException.class, () -> s4.tryAdvance(badAction)));
353         Assertions.assertEquals(currentSize - 1, s4.estimateSize(), "Spliterator should be advanced even when action throws");
354 
355         Assertions.assertSame(ex, Assertions.assertThrows(IllegalStateException.class, () -> s4.forEachRemaining(badAction)));
356         Assertions.assertEquals(0, s4.estimateSize(), "Spliterator should be finished even when action throws");
357         s4.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
358     }
359 }