RandomStringUtilsTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.lang3;

import static org.apache.commons.lang3.LangAssertions.assertIllegalArgumentException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

/**
 * Tests {@link RandomStringUtils}.
 */
class RandomStringUtilsTest extends AbstractLangTest {

    private static final int LOOP_COUNT = 1_000;
    /** Maximum safe value for count to avoid overflow: (21x + 3) / 5 + 10 < 0x0FFF_FFFF */
    private static final int MAX_SAFE_COUNT = 63_913_201;


    static Stream<RandomStringUtils> randomProvider() {
        return Stream.of(RandomStringUtils.secure(), RandomStringUtils.secureStrong(), RandomStringUtils.insecure());
    }

    /**
     * Computes Chi-Square statistic given observed and expected counts
     *
     * @param observed array of observed frequency counts
     * @param expected array of expected frequency counts
     */
    private double chiSquare(final int[] expected, final int[] observed) {
        double sumSq = 0.0d;
        for (int i = 0; i < observed.length; i++) {
            final double dev = observed[i] - expected[i];
            sumSq += dev * dev / expected[i];
        }
        return sumSq;
    }

    /**
     * Test for LANG-1286. Creates situation where old code would overflow a char and result in a code point outside the specified range.
     */
    @Test
    void testCharOverflow() {
        final int start = Character.MAX_VALUE;
        final int end = Integer.MAX_VALUE;

        @SuppressWarnings("serial")
        final Random fixedRandom = new Random() {
            @Override
            public int nextInt(final int n) {
                // Prevents selection of 'start' as the character
                return super.nextInt(n - 1) + 1;
            }
        };

        final String result = RandomStringUtils.random(2, start, end, false, false, null, fixedRandom);
        final int c = result.codePointAt(0);
        assertTrue(c >= start && c < end, String.format("Character '%d' not in range [%d,%d).", c, start, end));
    }

    @Test
    void testConstructor() {
        assertNotNull(new RandomStringUtils());
    }

    @Test
    void testExceptionsRandom() {
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1));
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1, true, true));
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1, new char[] { 'a' }));
        assertIllegalArgumentException(() -> RandomStringUtils.random(1, new char[0]));
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1, ""));
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1, (String) null));
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1, 'a', 'z', false, false));
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1, 'a', 'z', false, false, new char[] { 'a' }));
        assertIllegalArgumentException(() -> RandomStringUtils.random(-1, 'a', 'z', false, false, new char[] { 'a' }, new Random()));
        assertIllegalArgumentException(() -> RandomStringUtils.random(8, 32, 48, false, true));
        assertIllegalArgumentException(() -> RandomStringUtils.random(8, 32, 65, true, false));
        assertIllegalArgumentException(() -> RandomStringUtils.random(1, Integer.MIN_VALUE, -10, false, false, null));
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testExceptionsRandom(final RandomStringUtils rsu) {
        assertIllegalArgumentException(() -> rsu.next(-1));
        assertIllegalArgumentException(() -> rsu.next(-1, true, true));
        assertIllegalArgumentException(() -> rsu.next(-1, new char[] { 'a' }));
        assertIllegalArgumentException(() -> rsu.next(1, new char[0]));
        assertIllegalArgumentException(() -> rsu.next(-1, ""));
        assertIllegalArgumentException(() -> rsu.next(-1, (String) null));
        assertIllegalArgumentException(() -> rsu.next(-1, 'a', 'z', false, false));
        assertIllegalArgumentException(() -> rsu.next(-1, 'a', 'z', false, false, new char[] { 'a' }));
        assertIllegalArgumentException(() -> rsu.next(8, 32, 48, false, true));
        assertIllegalArgumentException(() -> rsu.next(8, 32, 65, true, false));
        assertIllegalArgumentException(() -> rsu.next(1, Integer.MIN_VALUE, -10, false, false, null));
    }

    @Test
    void testExceptionsRandomAlphabetic() {
        assertIllegalArgumentException(() -> RandomStringUtils.randomAlphabetic(-1));
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testExceptionsRandomAlphabetic(final RandomStringUtils rsu) {
        assertIllegalArgumentException(() -> rsu.nextAlphabetic(-1));
    }

    @Test
    void testExceptionsRandomAscii() {
        assertIllegalArgumentException(() -> RandomStringUtils.randomAscii(-1));
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testExceptionsRandomAscii(final RandomStringUtils rsu) {
        assertIllegalArgumentException(() -> rsu.nextAscii(-1));
    }

    @Test
    void testExceptionsRandomGraph() {
        assertIllegalArgumentException(() -> RandomStringUtils.randomGraph(-1));
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testExceptionsRandomGraph(final RandomStringUtils rsu) {
        assertIllegalArgumentException(() -> rsu.nextGraph(-1));
    }

    @Test
    void testExceptionsRandomNumeric() {
        assertIllegalArgumentException(() -> RandomStringUtils.randomNumeric(-1));
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testExceptionsRandomNumeric(final RandomStringUtils rsu) {
        assertIllegalArgumentException(() -> rsu.nextNumeric(-1));
    }

    @Test
    void testExceptionsRandomPrint() {
        assertIllegalArgumentException(() -> RandomStringUtils.randomPrint(-1));
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testExceptionsRandomPrint(final RandomStringUtils rsu) {
        assertIllegalArgumentException(() -> rsu.nextPrint(-1));
    }

    /**
     * Test homogeneity of random strings generated -- i.e., test that characters show up with expected frequencies in generated strings. Will fail randomly
     * about 1 in 100,000 times. Repeated failures indicate a problem.
     *
     * @param rsu the instance to test.
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testHomogeneity(final RandomStringUtils rsu) {
        final String set = "abc";
        final char[] chars = set.toCharArray();
        final int[] counts = { 0, 0, 0 };
        final int[] expected = { 200, 200, 200 };
        for (int i = 0; i < 100; i++) {
            final String gen = rsu.next(6, chars);
            for (int j = 0; j < 6; j++) {
                switch (gen.charAt(j)) {
                case 'a': {
                    counts[0]++;
                    break;
                }
                case 'b': {
                    counts[1]++;
                    break;
                }
                case 'c': {
                    counts[2]++;
                    break;
                }
                default: {
                    fail("generated character not in set");
                }
                }
            }
        }
        // Perform chi-square test with degrees of freedom = 3-1 = 2, testing at 1e-5 level.
        // This expects a failure rate of 1 in 100,000.
        // critical value: from scipy.stats import chi2; chi2(2).isf(1e-5)
        assertTrue(chiSquare(expected, counts) < 23.025850929940457d, "test homogeneity -- will fail about 1 in 100,000 times");
    }

    @ParameterizedTest
    @ValueSource(ints = {MAX_SAFE_COUNT, MAX_SAFE_COUNT + 1})
    @EnabledIfSystemProperty(named = "test.large.heap", matches = "true")
    void testHugeStrings(final int expectedLength) {
        final String hugeString = RandomStringUtils.random(expectedLength);
        assertEquals(expectedLength, hugeString.length(), "hugeString.length() == expectedLength");
    }

    /**
     * Checks if the string got by {@link RandomStringUtils#random(int)} can be converted to UTF-8 and back without loss.
     *
     * @see <a href="https://issues.apache.org/jira/browse/LANG-100">LANG-100</a>
     */
    @Test
    void testLang100() {
        final int size = 5000;
        final Charset charset = StandardCharsets.UTF_8;
        final String orig = RandomStringUtils.random(size);
        final byte[] bytes = orig.getBytes(charset);
        final String copy = new String(bytes, charset);

        // for a verbose compare:
        for (int i = 0; i < orig.length() && i < copy.length(); i++) {
            final char o = orig.charAt(i);
            final char c = copy.charAt(i);
            assertEquals(o, c, "differs at " + i + "(" + Integer.toHexString(Character.valueOf(o).hashCode()) + ","
                    + Integer.toHexString(Character.valueOf(c).hashCode()) + ")");
        }
        // compare length also
        assertEquals(orig.length(), copy.length());
        // just to be complete
        assertEquals(orig, copy);
    }

    /**
     * Checks if the string got by {@link RandomStringUtils#random(int)} can be converted to UTF-8 and back without loss.
     *
     * @param rsu the instance to test
     * @see <a href="https://issues.apache.org/jira/browse/LANG-100">LANG-100</a>
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testLang100(final RandomStringUtils rsu) {
        final int size = 5000;
        final Charset charset = StandardCharsets.UTF_8;
        final String orig = rsu.next(size);
        final byte[] bytes = orig.getBytes(charset);
        final String copy = new String(bytes, charset);

        // for a verbose compare:
        for (int i = 0; i < orig.length() && i < copy.length(); i++) {
            final char o = orig.charAt(i);
            final char c = copy.charAt(i);
            assertEquals(o, c, "differs at " + i + "(" + Integer.toHexString(Character.valueOf(o).hashCode()) + ","
                    + Integer.toHexString(Character.valueOf(c).hashCode()) + ")");
        }
        // compare length also
        assertEquals(orig.length(), copy.length());
        // just to be complete
        assertEquals(orig, copy);
    }

    @Test
    void testLANG805() {
        final long seedMillis = System.currentTimeMillis();
        assertEquals("aaa", RandomStringUtils.random(3, 0, 0, false, false, new char[] { 'a' }, new Random(seedMillis)));
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testLANG807(final RandomStringUtils rsu) {
        final IllegalArgumentException ex = assertIllegalArgumentException(() -> rsu.next(3, 5, 5, false, false));
        final String msg = ex.getMessage();
        assertTrue(msg.contains("start"), "Message (" + msg + ") must contain 'start'");
        assertTrue(msg.contains("end"), "Message (" + msg + ") must contain 'end'");
    }

    /**
     * Test {@code RandomStringUtils.random} works appropriately when letters=true
     * and the range does not only include ASCII letters.
     * Fails with probability less than 2^-40 (in practice this never happens).
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testNonASCIILetters(final RandomStringUtils rsu) {
        // Check that the following create a string with 10 characters 0x4e00 (a non-ASCII letter)
        String r1 = rsu.next(10, 0x4e00, 0x4e01, true, false);
        assertEquals(10, r1.length(), "wrong length");
        for (int i = 0; i < r1.length(); i++) {
            assertEquals(0x4e00, r1.charAt(i), "characters not all equal to 0x4e00");
        }

        // Same with both letters=true and numbers=true
        r1 = rsu.next(10, 0x4e00, 0x4e01, true, true);
        assertEquals(10, r1.length(), "wrong length");
        for (int i = 0; i < r1.length(); i++) {
            assertEquals(0x4e00, r1.charAt(i), "characters not all equal to 0x4e00");
        }

        // Check that at least one letter is not ASCII
        boolean found = false;
        r1 = rsu.next(40, 'F', 0x3000, true, false);
        assertEquals(40, r1.length(), "wrong length");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(Character.isLetter(r1.charAt(i)), "characters not all letters");
            if (r1.charAt(i) > 0x7f) {
                found = true;
            }
        }
        assertTrue(found, "no non-ASCII letter generated");
    }

    /**
     * Test {@code RandomStringUtils.random} works appropriately when numbers=true
     * and the range does not only include ASCII numbers/digits.
     * Fails with probability less than 2^-40 (in practice this never happens).
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testNonASCIINumbers(final RandomStringUtils rsu) {
        // Check that the following create a string with 10 characters 0x0660 (a non-ASCII digit)
        String r1 = rsu.next(10, 0x0660, 0x0661, false, true);
        assertEquals(10, r1.length(), "wrong length");
        for (int i = 0; i < r1.length(); i++) {
            assertEquals(0x0660, r1.charAt(i), "characters not all equal to 0x0660");
        }

        // Same with both letters=true and numbers=true
        r1 = rsu.next(10, 0x0660, 0x0661, true, true);
        assertEquals(10, r1.length(), "wrong length");
        for (int i = 0; i < r1.length(); i++) {
            assertEquals(0x0660, r1.charAt(i), "characters not all equal to 0x0660");
        }

        // Check that at least one letter is not ASCII
        boolean found = false;
        r1 = rsu.next(40, 'F', 0x3000, false, true);
        assertEquals(40, r1.length(), "wrong length");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(Character.isDigit(r1.charAt(i)), "characters not all numbers");
            if (r1.charAt(i) > 0x7f) {
                found = true;
            }
        }
        assertTrue(found, "no non-ASCII number generated");
    }

    /**
     * Make sure boundary alpha characters are generated by randomAlphabetic This test will fail randomly with probability = 4 * (51/52)**1000 ~ 1.58E-8
     */
    @Test
    void testRandomAlphabetic() {
        final char[] testChars = { 'a', 'z', 'A', 'Z' };
        final boolean[] found = { false, false, false, false };
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String randString = RandomStringUtils.randomAlphabetic(10);
            for (int j = 0; j < testChars.length; j++) {
                if (randString.indexOf(testChars[j]) > 0) {
                    found[j] = true;
                }
            }
        }
        for (int i = 0; i < testChars.length; i++) {
            assertTrue(found[i], "alphanumeric character not generated in 1000 attempts: " + testChars[i] + " -- repeated failures indicate a problem ");
        }
    }

    /**
     * Make sure boundary alpha characters are generated by randomAlphabetic This test will fail randomly with probability = 4 * (51/52)**1000 ~ 1.58E-8
     *
     * @param rsu the instance to test
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomAlphabetic(final RandomStringUtils rsu) {
        final char[] testChars = { 'a', 'z', 'A', 'Z' };
        final boolean[] found = { false, false, false, false };
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String randString = rsu.nextAlphabetic(10);
            for (int j = 0; j < testChars.length; j++) {
                if (randString.indexOf(testChars[j]) > 0) {
                    found[j] = true;
                }
            }
        }
        for (int i = 0; i < testChars.length; i++) {
            assertTrue(found[i], "alphanumeric character not generated in 1000 attempts: " + testChars[i] + " -- repeated failures indicate a problem ");
        }
    }

    @Test
    void testRandomAlphabeticRange() {
        final int expectedMinLengthInclusive = 1;
        final int expectedMaxLengthExclusive = 11;
        final String pattern = "^\\p{Alpha}{" + expectedMinLengthInclusive + ',' + expectedMaxLengthExclusive + "}$";

        int maxCreatedLength = expectedMinLengthInclusive;
        int minCreatedLength = expectedMaxLengthExclusive - 1;
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String s = RandomStringUtils.randomAlphabetic(expectedMinLengthInclusive, expectedMaxLengthExclusive);
            assertTrue(s.length() >= expectedMinLengthInclusive, "within range");
            assertTrue(s.length() <= expectedMaxLengthExclusive - 1, "within range");
            assertTrue(s.matches(pattern), s);

            if (s.length() < minCreatedLength) {
                minCreatedLength = s.length();
            }

            if (s.length() > maxCreatedLength) {
                maxCreatedLength = s.length();
            }
        }
        assertEquals(expectedMinLengthInclusive, minCreatedLength, "min generated, may fail randomly rarely");
        assertEquals(expectedMaxLengthExclusive - 1, maxCreatedLength, "max generated, may fail randomly rarely");
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomAlphabeticRange(final RandomStringUtils rsu) {
        final int expectedMinLengthInclusive = 1;
        final int expectedMaxLengthExclusive = 11;
        final String pattern = "^\\p{Alpha}{" + expectedMinLengthInclusive + ',' + expectedMaxLengthExclusive + "}$";

        int maxCreatedLength = expectedMinLengthInclusive;
        int minCreatedLength = expectedMaxLengthExclusive - 1;
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String s = rsu.nextAlphabetic(expectedMinLengthInclusive, expectedMaxLengthExclusive);
            assertTrue(s.length() >= expectedMinLengthInclusive, "within range");
            assertTrue(s.length() <= expectedMaxLengthExclusive - 1, "within range");

            assertTrue(s.matches(pattern), s);

            if (s.length() < minCreatedLength) {
                minCreatedLength = s.length();
            }

            if (s.length() > maxCreatedLength) {
                maxCreatedLength = s.length();
            }
        }
        assertEquals(expectedMinLengthInclusive, minCreatedLength, "min generated, may fail randomly rarely");
        assertEquals(expectedMaxLengthExclusive - 1, maxCreatedLength, "max generated, may fail randomly rarely");
    }

    /**
     * Make sure boundary alphanumeric characters are generated by randomAlphaNumeric This test will fail randomly with probability = 6 * (61/62)**1000 ~ 5.2E-7
     */
    @Test
    void testRandomAlphaNumeric() {
        final char[] testChars = { 'a', 'z', 'A', 'Z', '0', '9' };
        final boolean[] found = { false, false, false, false, false, false };
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String randString = RandomStringUtils.randomAlphanumeric(10);
            for (int j = 0; j < testChars.length; j++) {
                if (randString.indexOf(testChars[j]) > 0) {
                    found[j] = true;
                }
            }
        }
        for (int i = 0; i < testChars.length; i++) {
            assertTrue(found[i], "alphanumeric character not generated in 1000 attempts: " + testChars[i] + " -- repeated failures indicate a problem ");
        }
    }

    /**
     * Make sure boundary alphanumeric characters are generated by randomAlphaNumeric This test will fail randomly with probability = 6 * (61/62)**1000 ~ 5.2E-7
     *
     * @param rsu the instance to test
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomAlphaNumeric(final RandomStringUtils rsu) {
        final char[] testChars = { 'a', 'z', 'A', 'Z', '0', '9' };
        final boolean[] found = { false, false, false, false, false, false };
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String randString = rsu.nextAlphanumeric(10);
            for (int j = 0; j < testChars.length; j++) {
                if (randString.indexOf(testChars[j]) > 0) {
                    found[j] = true;
                }
            }
        }
        for (int i = 0; i < testChars.length; i++) {
            assertTrue(found[i], "alphanumeric character not generated in 1000 attempts: " + testChars[i] + " -- repeated failures indicate a problem ");
        }
    }

    @Test
    void testRandomAlphanumericRange() {
        final int expectedMinLengthInclusive = 1;
        final int expectedMaxLengthExclusive = 11;
        final String pattern = "^\\p{Alnum}{" + expectedMinLengthInclusive + ',' + expectedMaxLengthExclusive + "}$";

        int maxCreatedLength = expectedMinLengthInclusive;
        int minCreatedLength = expectedMaxLengthExclusive - 1;
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String s = RandomStringUtils.randomAlphanumeric(expectedMinLengthInclusive, expectedMaxLengthExclusive);
            assertTrue(s.length() >= expectedMinLengthInclusive, "within range");
            assertTrue(s.length() <= expectedMaxLengthExclusive - 1, "within range");
            assertTrue(s.matches(pattern), s);

            if (s.length() < minCreatedLength) {
                minCreatedLength = s.length();
            }

            if (s.length() > maxCreatedLength) {
                maxCreatedLength = s.length();
            }
        }
        assertEquals(expectedMinLengthInclusive, minCreatedLength, "min generated, may fail randomly rarely");
        assertEquals(expectedMaxLengthExclusive - 1, maxCreatedLength, "max generated, may fail randomly rarely");
    }

    /**
     * Test the implementation
     *
     * @param rsu the instance to test.
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomApis(final RandomStringUtils rsu) {
        String r1 = rsu.next(50);
        assertEquals(50, r1.length(), "random(50) length");
        String r2 = rsu.next(50);
        assertEquals(50, r2.length(), "random(50) length");
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.nextAscii(50);
        assertEquals(50, r1.length(), "randomAscii(50) length");
        for (int i = 0; i < r1.length(); i++) {
            final int ch = r1.charAt(i);
            assertTrue(ch >= 32, "char >= 32");
            assertTrue(ch <= 127, "char <= 127");
        }
        r2 = rsu.nextAscii(50);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.nextAlphabetic(50);
        assertEquals(50, r1.length(), "randomAlphabetic(50)");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(Character.isLetter(r1.charAt(i)) && !Character.isDigit(r1.charAt(i)), "r1 contains alphabetic");
        }
        r2 = rsu.nextAlphabetic(50);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.nextAlphanumeric(50);
        assertEquals(50, r1.length(), "randomAlphanumeric(50)");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(Character.isLetterOrDigit(r1.charAt(i)), "r1 contains alphanumeric");
        }
        r2 = rsu.nextAlphabetic(50);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.nextGraph(50);
        assertEquals(50, r1.length(), "randomGraph(50) length");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(r1.charAt(i) >= 33 && r1.charAt(i) <= 126, "char between 33 and 126");
        }
        r2 = rsu.nextGraph(50);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.nextNumeric(50);
        assertEquals(50, r1.length(), "randomNumeric(50)");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(Character.isDigit(r1.charAt(i)) && !Character.isLetter(r1.charAt(i)), "r1 contains numeric");
        }
        r2 = rsu.nextNumeric(50);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.nextPrint(50);
        assertEquals(50, r1.length(), "randomPrint(50) length");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(r1.charAt(i) >= 32 && r1.charAt(i) <= 126, "char between 32 and 126");
        }
        r2 = rsu.nextPrint(50);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        String set = "abcdefg";
        r1 = rsu.next(50, set);
        assertEquals(50, r1.length(), "random(50, \"abcdefg\")");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(set.indexOf(r1.charAt(i)) > -1, "random char in set");
        }
        r2 = rsu.next(50, set);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.next(50, (String) null);
        assertEquals(50, r1.length(), "random(50) length");
        r2 = rsu.next(50, (String) null);
        assertEquals(50, r2.length(), "random(50) length");
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        set = "stuvwxyz";
        r1 = rsu.next(50, set.toCharArray());
        assertEquals(50, r1.length(), "random(50, \"stuvwxyz\")");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(set.indexOf(r1.charAt(i)) > -1, "random char in set");
        }
        r2 = rsu.next(50, set);
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.next(50, (char[]) null);
        assertEquals(50, r1.length(), "random(50) length");
        r2 = rsu.next(50, (char[]) null);
        assertEquals(50, r2.length(), "random(50) length");
        assertNotEquals(r1, r2, "!r1.equals(r2)");

        r1 = rsu.next(0);
        assertEquals("", r1, "random(0).equals(\"\")");
    }

    /**
     * Make sure 32 and 127 are generated by randomNumeric This test will fail randomly with probability = 2*(95/96)**1000 ~ 5.7E-5
     *
     * @param rsu the instance to test
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomAscii(final RandomStringUtils rsu) {
        final char[] testChars = { (char) 32, (char) 126 };
        final boolean[] found = { false, false };
        // Test failures have been observed on GitHub builds with a 100 limit.
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String randString = rsu.nextAscii(10);
            for (int j = 0; j < testChars.length; j++) {
                if (randString.indexOf(testChars[j]) > 0) {
                    found[j] = true;
                }
            }
        }
        for (int i = 0; i < testChars.length; i++) {
            assertTrue(found[i], "ascii character not generated in 1000 attempts: " + (int) testChars[i] + " -- repeated failures indicate a problem");
        }
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomAsciiRange(final RandomStringUtils rsu) {
        final int expectedMinLengthInclusive = 1;
        final int expectedMaxLengthExclusive = 11;
        final String pattern = "^\\p{ASCII}{" + expectedMinLengthInclusive + ',' + expectedMaxLengthExclusive + "}$";

        int maxCreatedLength = expectedMinLengthInclusive;
        int minCreatedLength = expectedMaxLengthExclusive - 1;
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String s = rsu.nextAscii(expectedMinLengthInclusive, expectedMaxLengthExclusive);
            assertTrue(s.length() >= expectedMinLengthInclusive, "within range");
            assertTrue(s.length() <= expectedMaxLengthExclusive - 1, "within range");
            assertTrue(s.matches(pattern), s);

            if (s.length() < minCreatedLength) {
                minCreatedLength = s.length();
            }

            if (s.length() > maxCreatedLength) {
                maxCreatedLength = s.length();
            }
        }
        assertEquals(expectedMinLengthInclusive, minCreatedLength, "min generated, may fail randomly rarely");
        assertEquals(expectedMaxLengthExclusive - 1, maxCreatedLength, "max generated, may fail randomly rarely");
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomGraphRange(final RandomStringUtils rsu) {
        final int expectedMinLengthInclusive = 1;
        final int expectedMaxLengthExclusive = 11;
        final String pattern = "^\\p{Graph}{" + expectedMinLengthInclusive + ',' + expectedMaxLengthExclusive + "}$";

        int maxCreatedLength = expectedMinLengthInclusive;
        int minCreatedLength = expectedMaxLengthExclusive - 1;
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String s = rsu.nextGraph(expectedMinLengthInclusive, expectedMaxLengthExclusive);
            assertTrue(s.length() >= expectedMinLengthInclusive, "within range");
            assertTrue(s.length() <= expectedMaxLengthExclusive - 1, "within range");
            assertTrue(s.matches(pattern), s);

            if (s.length() < minCreatedLength) {
                minCreatedLength = s.length();
            }

            if (s.length() > maxCreatedLength) {
                maxCreatedLength = s.length();
            }
        }
        assertEquals(expectedMinLengthInclusive, minCreatedLength, "min generated, may fail randomly rarely");
        assertEquals(expectedMaxLengthExclusive - 1, maxCreatedLength, "max generated, may fail randomly rarely");
    }

    /**
     * Make sure '0' and '9' are generated by randomNumeric This test will fail randomly with probability = 2 * (9/10)**1000 ~ 3.5E-46
     *
     * @param rsu the instance to test
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomNumeric(final RandomStringUtils rsu) {
        final char[] testChars = { '0', '9' };
        final boolean[] found = { false, false };
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String randString = rsu.nextNumeric(10);
            for (int j = 0; j < testChars.length; j++) {
                if (randString.indexOf(testChars[j]) > 0) {
                    found[j] = true;
                }
            }
        }
        for (int i = 0; i < testChars.length; i++) {
            assertTrue(found[i], "digit not generated in 1000 attempts: " + testChars[i] + " -- repeated failures indicate a problem ");
        }
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomNumericRange(final RandomStringUtils rsu) {
        final int expectedMinLengthInclusive = 1;
        final int expectedMaxLengthExclusive = 11;
        final String pattern = "^\\p{Digit}{" + expectedMinLengthInclusive + ',' + expectedMaxLengthExclusive + "}$";

        int maxCreatedLength = expectedMinLengthInclusive;
        int minCreatedLength = expectedMaxLengthExclusive - 1;
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String s = rsu.nextNumeric(expectedMinLengthInclusive, expectedMaxLengthExclusive);
            assertTrue(s.length() >= expectedMinLengthInclusive, "within range");
            assertTrue(s.length() <= expectedMaxLengthExclusive - 1, "within range");
            assertTrue(s.matches(pattern), s);

            if (s.length() < minCreatedLength) {
                minCreatedLength = s.length();
            }

            if (s.length() > maxCreatedLength) {
                maxCreatedLength = s.length();
            }
        }
        assertEquals(expectedMinLengthInclusive, minCreatedLength, "min generated, may fail randomly rarely");
        assertEquals(expectedMaxLengthExclusive - 1, maxCreatedLength, "max generated, may fail randomly rarely");
    }

    @Test
    void testRandomParameter() {
        final long seedMillis = System.currentTimeMillis();
        final String r1 = RandomStringUtils.random(50, 0, 0, true, true, null, new Random(seedMillis));
        final String r2 = RandomStringUtils.random(50, 0, 0, true, true, null, new Random(seedMillis));
        assertEquals(r1, r2, "r1.equals(r2)");
    }

    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomPrintRange(final RandomStringUtils rsu) {
        final int expectedMinLengthInclusive = 1;
        final int expectedMaxLengthExclusive = 11;
        final String pattern = "^\\p{Print}{" + expectedMinLengthInclusive + ',' + expectedMaxLengthExclusive + "}$";

        int maxCreatedLength = expectedMinLengthInclusive;
        int minCreatedLength = expectedMaxLengthExclusive - 1;
        for (int i = 0; i < LOOP_COUNT; i++) {
            final String s = rsu.nextPrint(expectedMinLengthInclusive, expectedMaxLengthExclusive);
            assertTrue(s.length() >= expectedMinLengthInclusive, "within range");
            assertTrue(s.length() <= expectedMaxLengthExclusive - 1, "within range");
            assertTrue(s.matches(pattern), s);

            if (s.length() < minCreatedLength) {
                minCreatedLength = s.length();
            }

            if (s.length() > maxCreatedLength) {
                maxCreatedLength = s.length();
            }
        }
        assertEquals(expectedMinLengthInclusive, minCreatedLength, "min generated, may fail randomly rarely");
        assertEquals(expectedMaxLengthExclusive - 1, maxCreatedLength, "max generated, may fail randomly rarely");
    }

    /**
     * Test {@code RandomStringUtils.random} works appropriately when chars specified.
     *
     * @param rsu the instance to test.
     */
    @ParameterizedTest
    @MethodSource("randomProvider")
    void testRandomWithChars(final RandomStringUtils rsu) {
        final char[] digitChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
        final String r1 = rsu.next(50, 0, 0, true, true, digitChars);
        assertEquals(50, r1.length(), "randomNumeric(50)");
        for (int i = 0; i < r1.length(); i++) {
            assertTrue(
                    Character.isDigit(r1.charAt(i)) && !Character.isLetter(r1.charAt(i)),
                    "r1 contains numeric");
        }
        final String r2 = rsu.nextNumeric(50);
        assertNotEquals(r1, r2);

        final String r3 = rsu.next(50, 0, 0, true, true, digitChars);
        assertNotEquals(r1, r3);
        assertNotEquals(r2, r3);
    }
}