Skip to content

Commit

Permalink
Implement wrapper around random_bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
ancarda committed Mar 30, 2021
1 parent 599d7fc commit ec4bd3e
Show file tree
Hide file tree
Showing 14 changed files with 397 additions and 0 deletions.
Binary file not shown.
32 changes: 32 additions & 0 deletions src/RandomBytes/Callback.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
22 changes: 22 additions & 0 deletions src/RandomBytes/Failure.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
27 changes: 27 additions & 0 deletions src/RandomBytes/Fixed.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
29 changes: 29 additions & 0 deletions src/RandomBytes/OneShot.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions src/RandomBytes/RandomBytes.php
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 19 additions & 0 deletions src/RandomBytes/Real.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
57 changes: 57 additions & 0 deletions src/RandomBytes/Succession.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions tests/RandomBytes/CallbackTest.php
Original file line number Diff line number Diff line change
@@ -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));
}
}
21 changes: 21 additions & 0 deletions tests/RandomBytes/FailureTest.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
28 changes: 28 additions & 0 deletions tests/RandomBytes/FixedTest.php
Original file line number Diff line number Diff line change
@@ -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));
}
}
32 changes: 32 additions & 0 deletions tests/RandomBytes/OneShotTest.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
26 changes: 26 additions & 0 deletions tests/RandomBytes/RealTest.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Loading

0 comments on commit ec4bd3e

Please sign in to comment.