~ancarda/high-test-coverage

ec4bd3ec6614fb00b0757f92d4fbc6eb0997c88a — Mark Dain 2 years ago 599d7fc 1.1
Implement wrapper around random_bytes
D src/DateTimeImmutable/.DateTimeImmutableFactory.php.swp => src/DateTimeImmutable/.DateTimeImmutableFactory.php.swp +0 -0
A src/RandomBytes/Callback.php => src/RandomBytes/Callback.php +32 -0
@@ 0,0 1,32 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\RandomBytes;

/**
 * Dispatch to a user function.
 *
 * This implementation calls the function given to the constructor every time
 * random bytes are requested. The user function is given the arguments
 * provided to randomBytes. This class is intended to be used when you need
 * arbitary or complex logic, but don't want to mock the RandomBytes interface.
 *
 * Please note that there are many implementations of RandomBytes including
 * Succession and OneShot that may implement the logic you are looking for.
 */
final class Callback implements RandomBytes
{
    /** @var callable */
    private $cb;

    public function __construct(callable $cb)
    {
        $this->cb = $cb;
    }

    public function __invoke(int $length): string
    {
        return call_user_func($this->cb, $length);
    }
}

A src/RandomBytes/Failure.php => src/RandomBytes/Failure.php +22 -0
@@ 0,0 1,22 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\RandomBytes;

use RuntimeException;

/**
 * Fail to generate random bytes every time
 *
 * This class always throws an exception when you request random bytes. It's
 * intended to be used to test how your code behaves when randomness is not
 * available.
 */
final class Failure implements RandomBytes
{
    public function __invoke(int $length): string
    {
        throw new RuntimeException('Could not gather sufficient random data');
    }
}

A src/RandomBytes/Fixed.php => src/RandomBytes/Fixed.php +27 -0
@@ 0,0 1,27 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\RandomBytes;

/**
 * Return a predetermined fixed value every time
 *
 * This is the simplest possible implementation of RandomBytes.
 * The value given in the constructor is returned from invoke every time.
 */
final class Fixed implements RandomBytes
{
    /** @var string */
    private $value;

    public function __construct(string $value)
    {
        $this->value = $value;
    }

    public function __invoke(int $length): string
    {
        return $this->value;
    }
}

A src/RandomBytes/OneShot.php => src/RandomBytes/OneShot.php +29 -0
@@ 0,0 1,29 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\RandomBytes;

/**
 * Generate a single random string that is returned forever
 *
 * OneShot generates a single real random string, then returns that -- and
 * always that -- forever.
 *
 * This is intended to be used when you need uniformity across a test run, but
 * can have or want randomness between test runs.
 */
final class OneShot implements RandomBytes
{
    /** @var string|null */
    private $value = null;

    public function __invoke(int $length): string
    {
        if ($this->value === null) {
            $this->value = random_bytes($length);
        }

        return $this->value;
    }
}

A src/RandomBytes/RandomBytes.php => src/RandomBytes/RandomBytes.php +25 -0
@@ 0,0 1,25 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\RandomBytes;

/**
 * Mockable wrapper around random_bytes
 *
 * You should typehint with this interface in all your code. A typical use
 * would be to have a constructor accept an instance like so:
 *
 *     function __construct(RandomBytes $randomBytes)
 *
 * Which is then used throughout a class. Your Dependency Injection container
 * would then have an entry that resolves to Real:
 *
 *     RandomBytes::class => Real::class,
 *
 * When that class is under test, you'll instead give it a class like Fixed.
 */
interface RandomBytes
{
    public function __invoke(int $length): string;
}

A src/RandomBytes/Real.php => src/RandomBytes/Real.php +19 -0
@@ 0,0 1,19 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\RandomBytes;

/**
 * Generate real random bytes
 *
 * This class just wraps random_bytes and is intended to be used in production
 * when you need a real random byte generator that you can mock.
 */
final class Real implements RandomBytes
{
    public function __invoke(int $length): string
    {
        return random_bytes($length);
    }
}

A src/RandomBytes/Succession.php => src/RandomBytes/Succession.php +57 -0
@@ 0,0 1,57 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\RandomBytes;

use LogicException;

/**
 * Return the next item in a set of predetermined fixed values every time
 *
 * This implementation takes a list of strings in the constructor. Each time
 * a random string is requested, the next item in the list is returned and
 * the pointer is moved one place.
 *
 * When the list is exhausted, the pointer wraps around.
 */
final class Succession implements RandomBytes
{
    /** @var array<int, string> */
    private $succession = [];

    /** @var int */
    private $cursor = 0;

    /** @var int */
    private $last = 0;

    /**
     * @param array<int, string> $succession Non-Empty array
     * @throws LogicException If given an empty array
     */
    public function __construct(array $succession)
    {
        if (count($succession) === 0) {
            throw new LogicException('succession cannot be empty');
        }

        $this->last = count($succession) - 1;
        $this->succession = $succession;
    }

    public function __invoke(int $length): string
    {
        if ($this->cursor === $this->last) {
            $this->cursor = 0;
            return $this->succession[$this->last];
        }

        return $this->succession[$this->cursor++];
    }

    public function rewind(): void
    {
        $this->cursor = 0;
    }
}

A tests/RandomBytes/CallbackTest.php => tests/RandomBytes/CallbackTest.php +25 -0
@@ 0,0 1,25 @@
<?php

declare(strict_types=1);

namespace Tests\RandomBytes;

use Ancarda\HighTestCoverage\RandomBytes\Callback;
use PHPUnit\Framework\TestCase;

final class CallbackTest extends TestCase
{
    public function testCallback(): void
    {
        $map = new Callback(function (int $code): string {
            if ($code === 1) {
                return 'Yes';
            }

            return 'No';
        });

        self::assertSame('Yes', $map(1));
        self::assertSame('No', $map(2));
    }
}

A tests/RandomBytes/FailureTest.php => tests/RandomBytes/FailureTest.php +21 -0
@@ 0,0 1,21 @@
<?php

declare(strict_types=1);

namespace Tests\RandomBytes;

use Ancarda\HighTestCoverage\RandomBytes\Failure;
use PHPUnit\Framework\TestCase;
use RuntimeException;

final class FailureTest extends TestCase
{
    public function testThrowsException(): void
    {
        $failure = new Failure();

        $this->expectException(RuntimeException::class);

        $failure(6);
    }
}

A tests/RandomBytes/FixedTest.php => tests/RandomBytes/FixedTest.php +28 -0
@@ 0,0 1,28 @@
<?php

declare(strict_types=1);

namespace Tests\RandomBytes;

use Ancarda\HighTestCoverage\RandomBytes\Fixed;
use PHPUnit\Framework\TestCase;

final class FixedTest extends TestCase
{
    public function testFixed(): void
    {
        $fixed = new Fixed('abc');

        $a = $fixed(3);
        $b = $fixed(3);

        self::assertSame($a, $b);
    }

    public function testReturnFixedValueOutsideRange(): void
    {
        $fixed = new Fixed('abcdef');

        self::assertSame('abcdef', $fixed(6));
    }
}

A tests/RandomBytes/OneShotTest.php => tests/RandomBytes/OneShotTest.php +32 -0
@@ 0,0 1,32 @@
<?php

declare(strict_types=1);

namespace Tests\RandomBytes;

use Ancarda\HighTestCoverage\RandomBytes\OneShot;
use PHPUnit\Framework\TestCase;

final class OneShotTest extends TestCase
{
    public function testOneShotAlwaysReturnsSameString(): void
    {
        $oneShot = new OneShot();

        $output = $oneShot(10);
        self::assertSame($output, $oneShot(10));
    }

    public function testLooksRandom(): void
    {
        while (true) {
            $a = (new OneShot())(32);
            $b = (new OneShot())(32);

            if ($a !== $b) {
                self::assertNotSame($a, $b);
                return;
            }
        }
    }
}

A tests/RandomBytes/RealTest.php => tests/RandomBytes/RealTest.php +26 -0
@@ 0,0 1,26 @@
<?php

declare(strict_types=1);

namespace Tests\RandomBytes;

use Ancarda\HighTestCoverage\RandomBytes\Real;
use PHPUnit\Framework\TestCase;

final class RealTest extends TestCase
{
    public function testLooksRandom(): void
    {
        $real = new Real();

        while (true) {
            $c = $real(0xFF);
            $d = $real(0xFF);

            if ($c !== $d) {
                self::assertNotSame($c, $d);
                return;
            }
        }
    }
}

A tests/RandomBytes/SuccessionTest.php => tests/RandomBytes/SuccessionTest.php +54 -0
@@ 0,0 1,54 @@
<?php

declare(strict_types=1);

namespace Tests\RandomBytes;

use Ancarda\HighTestCoverage\RandomBytes\Succession;
use LogicException;
use PHPUnit\Framework\TestCase;

final class SuccessionTest extends TestCase
{
    public function testSuccession(): void
    {
        $succession = new Succession(['red', 'yellow', 'green', 'blue']);

        self::assertSame('red', $succession(1));
        self::assertSame('yellow', $succession(1));
        self::assertSame('green', $succession(1));
        self::assertSame('blue', $succession(1));
    }

    public function testSuccessionWrapsAround(): void
    {
        $succession = new Succession(['red', 'yellow', 'green']);

        for ($i = 0; $i <= 3; $i++) {
            self::assertSame('red', $succession(1));
            self::assertSame('yellow', $succession(1));
            self::assertSame('green', $succession(1));
        }
    }

    public function testRewind(): void
    {
        $succession = new Succession(['red', 'yellow', 'green', 'blue']);

        self::assertSame('red', $succession(1));
        self::assertSame('yellow', $succession(1));
        $succession->rewind();
        self::assertSame('red', $succession(1));
        self::assertSame('yellow', $succession(1));
        self::assertSame('green', $succession(1));
        self::assertSame('blue', $succession(1));
    }

    public function testSuccessionRejectsEmptyArrays(): void
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('succession cannot be empty');

        new Succession([]);
    }
}