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([]);
+ }
+}