Is there a random number generator that can be used for repeatable testing in PHP?

Viewed 81

Currently we are using the built in random number generator that can be seeded with mt_srand(), but that means our tests are dependent on global state.

Is there anyway using the built-in random number generators that avoid being dependent on global state, and so aren't so flaky when it comes to repeatability?

2 Answers

From PHP 8.2 you are able to use the new random extension.

This extension was designed to address some of the types of issue you are experiencing with the current random number generators in PHP. The details of it are:

It is object based

Because the state of the random number generators (called “Engines”) is stored in an object, they can be passed as parameters, and won't interfere with each other, as they are not dependent on global state.

So you can create multiple random engines which implement the Xoshiro256** algorithm with the same or different seeds:

$engine_1 = new \Random\Engine\Xoshiro256StarStar(1234);
$engine_2 = new \Random\Engine\Xoshiro256StarStar(4321); // generates a different sequence than (1).
$engine_3 = new \Random\Engine\Xoshiro256StarStar(1234); // generates the same sequence as (1).

Each of these engines will generate their own reproducible sequence of random data, without interfering with the internal state of each other.

Better algorithms

The Mersenne Twister random generator was state of the art for 1997, but mt_rand() fails several statistical tests for randomness, e.g. the BigCrush and Crush tests.

The following is the list of the engines available from PHP 8.2 on.

\Random\Engine\Mt19937

This engine implements the same Mersenne Twister that's currently available as mt_rand().

The Mt19937 engine still generates a 32 bit (4 byte) bytestring, as Mt19937 by definition is a 32 bit engine.

However combining it with the the Randomizer, which is the high-level API used to interact with the engines, the Mt19937 engine is capable of generating random 64 bit integers, by stretching the engine's randomness into whatever amount of randomness is required to span the requested range:

$randomizer = new \Random\Randomizer(new \Random\Engine\Mt19937(1234));
$randomizer->getInt(0, 8_000_000_000); // 8 billion requires 64 bit integers

\Random\Engine\PcgOneseq128XslRr64

This engine implements a Permuted Congruential Generator (pcg_oneseq_128_xsl_rr_64).

PCG is a family of simple fast space-efficient statistically good algorithms for random number generation. Unlike many general-purpose RNGs, they are also hard to predict.

It is a pseudorandom number generator (PRNG) and so does not generate cryptographically secure random sequences.

More info can be found here: https://www.pcg-random.org/

\Random\Engine\Xoshiro256StarStar

This is another pseudorandom number generator and so again does not generate cryptographically secure random sequences.

It is called Xoshiro256StarStar in PHP, but in general the name of the algorithm is spelled as 'Xoshiro256**'. This is due to PHP not support the character * in class names

The full details of the algorithm can be found here: https://prng.di.unimi.it/

\Random\Engine\Secure

This engine implements a cryptographically secure PRNG (CSPRNG). It cannot be seeded, as its purpose is to generate the highest quality, unguessable randomness required for cryptographic applications (e.g. password reset links).

What engine to use

In order of preference:

  1. Use Secure. This is the safe choice, unless you know you have specific requirements. That's why it's the default for the Randomizer if no engine is given.

  2. Use either Xoshiro256StarStar or PcgOneseq128XslRr64 if your application does not require a CSPRNG and you have strict performance requirements, or if your application requires the random numbers to be repeatable, for reproducibility.

  3. Use Mt19937 only for backwards compatibility. Both Xoshiro256** and PcgOneseq128XslRr64 are better in every metric possible.

You can also choose to use a reproducible random number generator in your development and testing environments, and then use a cyptographically secure generator in production:

$rng = $is_production
  ? new \Random\Engine\Secure()
  : new \Random\Engine\PcgOneseq128XslRr64(1234);

Custom implementations

If the default engines are not sufficient to support your use case you can also implement your own engine in userland PHP.

Here's a simple example of a SHA-1 based engine:

<?php

final class Sha1Engine implements \Random\Engine {

    public function __construct(private string $state) { }

    /**
     * Return a random bytestring. The bytestring will be interpreted in little-endian order.
     */
    public function generate(): string
    {
        $this->state = \sha1($this->state, true);

        return \substr($this->state, 0, 8);
    }
}

The randomness generated by this SHA-1 based engine should be fairly good, but is must not be used for security-critical applications. The Secure engine must be used!

Create a 32 bit random number by seed based on Lehmer algorithm. Useful for real procedural generation.

Get a random number with seed of 1000

$random = lgc_rand(1000);

gives

576869358

Use modulo to force a range 0-256

$random = lgc_rand(1000) % 256;

gives

238
function lgc_rand(int $seed = 1): int {
    return ($seed + 1) * 279470273 % 0xfffffffb;
}
Related