Implement Unit Tests, add Readme, add examples, stronger implementation

This commit is contained in:
2020-12-11 01:25:38 +01:00
commit ffebc74a7d
19 changed files with 3149 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
<?php
namespace Toalett\Multiprocessing\Tests;
use PHPUnit\Framework\TestCase;
use Toalett\Multiprocessing\ConcurrencyLimit;
use Toalett\Multiprocessing\Exception\InvalidArgumentException;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
class ConcurrencyLimitTest extends TestCase
{
use PropertyInspector;
public function testItDoesNotAllowZeroAsLimit(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected -1 or positive integer, got \'0\'');
new ConcurrencyLimit(0);
}
public function testItDoesAllowNegativeOneAsLimit(): void
{
$limit = new ConcurrencyLimit(-1);
self::assertTrue($limit->isUnlimited());
}
/**
* @param int $negativeNumber
* @dataProvider negativeValueProvider
*/
public function testItDoesNotAllowAnyOtherNegativeNumberAsLimitExceptNegativeOne(int $negativeNumber): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Expected -1 or positive integer, got \'%s\'', $negativeNumber));
new ConcurrencyLimit($negativeNumber);
}
public function testItCanBeMadeUnlimited(): void
{
$limit = ConcurrencyLimit::unlimited();
self::assertTrue($limit->isUnlimited());
}
public function testItCanLimitToASingleWorker(): void
{
$limit = ConcurrencyLimit::singleWorker();
self::assertFalse($limit->isUnlimited());
self::assertEquals(1, $this->getProperty($limit, 'limit'));
}
public function testAnUnlimitedLimitCanNeverBeReached(): void
{
$limit = ConcurrencyLimit::unlimited();
self::assertFalse($limit->isReachedBy(PHP_INT_MIN));
self::assertFalse($limit->isReachedBy(0));
self::assertFalse($limit->isReachedBy(PHP_INT_MAX));
}
public function testABoundLimitCanBeReached(): void
{
$three = new ConcurrencyLimit(3);
$seven = new ConcurrencyLimit(7);
self::assertTrue($three->isReachedBy(3));
self::assertFalse($three->isReachedBy(2));
self::assertFalse($three->isReachedBy(1));
self::assertTrue($seven->isReachedBy(7));
self::assertTrue($seven->isReachedBy(120));
self::assertFalse($seven->isReachedBy(-2));
}
public function negativeValueProvider(): array
{
return [
'-2' => [-2],
'-3' => [-3],
'-10000' => [-10000],
'PHP_INT_MIN' => [PHP_INT_MIN],
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Toalett\Multiprocessing\Tests;
use PHPUnit\Framework\TestCase;
use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\ConcurrencyLimit;
use Toalett\Multiprocessing\ContextBuilder;
use Toalett\Multiprocessing\Tests\Tools\PropertyInspector;
class ContextBuilderTest extends TestCase
{
use PropertyInspector;
public function testItIsImmutable(): void
{
$builder = ContextBuilder::create();
$eventLoop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class);
self::assertNotSame($builder->withEventLoop($eventLoop), $builder);
self::assertNotSame($builder->withLimit($limit), $builder);
}
public function testItGivesBackANewContextEachTimeBuildIsInvoked(): void
{
$builder = ContextBuilder::create();
self::assertNotSame($builder->build(), $builder->build());
}
public function testItCreatesANewContextWithUnlimitedConcurrencyWhenSupplyingNoArguments(): void
{
$builder = ContextBuilder::create();
$context = $builder->build();
self::assertIsObject($context);
self::assertInstanceOf(LoopInterface::class, $this->getProperty($context, 'eventLoop'));
/** @var ConcurrencyLimit|null $limit */
$limit = $this->getProperty($context, 'limit');
self::assertIsObject($limit);
self::assertInstanceOf(ConcurrencyLimit::class, $limit);
self::assertTrue($limit->isUnlimited());
}
public function testWhenSuppliedWithACustomEventLoopItUsesThatEventLoop(): void
{
$builder = ContextBuilder::create();
$eventLoop = $this->createMock(LoopInterface::class);
$context = $builder->withEventLoop($eventLoop)->build();
$usedEventLoop = $this->getProperty($context, 'eventLoop');
self::assertSame($eventLoop, $usedEventLoop);
}
public function testWhenSuppliedWithACustomConcurrencyLimitItUsesThatLimit(): void
{
$builder = ContextBuilder::create();
$limit = $this->createMock(ConcurrencyLimit::class);
$context = $builder->withLimit($limit)->build();
$usedLimit = $this->getProperty($context, 'limit');
self::assertSame($limit, $usedLimit);
}
}

98
src/Tests/ContextTest.php Normal file
View File

@@ -0,0 +1,98 @@
<?php
namespace Toalett\Multiprocessing\Tests;
use PHPUnit\Framework\TestCase;
use React\EventLoop\Factory;
use React\EventLoop\LoopInterface;
use Toalett\Multiprocessing\ConcurrencyLimit;
use Toalett\Multiprocessing\Context;
use Toalett\Multiprocessing\Workers;
class ContextTest extends TestCase
{
public function testItEmitsAnEventWhenBooted(): void
{
$limit = $this->createMock(ConcurrencyLimit::class);
$loop = Factory::create();
$context = new Context($loop, $limit);
$loop->futureTick(fn() => $context->stop());
$bootEventHasTakenPlace = false;
$context->on('booted', function () use (&$bootEventHasTakenPlace) {
$bootEventHasTakenPlace = true;
});
self::assertFalse($bootEventHasTakenPlace);
$context->run();
self::assertTrue($bootEventHasTakenPlace);
}
public function testItEmitsEventsWhenCongestionOccursAndIsRelieved(): void
{
$loop = Factory::create();
$limit = $this->createMock(ConcurrencyLimit::class);
$context = new Context($loop, $limit);
$limit->method('isReachedBy')->willReturn(true); // trigger congestion
$loop->futureTick(fn() => $context->stop());
$congestionEventHasTakenPlace = false;
$congestionRelievedEventHasTakenPlace = false;
$context->on('congestion', function () use (&$congestionEventHasTakenPlace) {
$congestionEventHasTakenPlace = true;
});
$context->on('congestion_relieved', function () use (&$congestionRelievedEventHasTakenPlace) {
$congestionRelievedEventHasTakenPlace = true;
});
self::assertFalse($congestionEventHasTakenPlace);
self::assertFalse($congestionRelievedEventHasTakenPlace);
$context->submit(fn() => []);
$context->run();
self::assertTrue($congestionEventHasTakenPlace);
self::assertTrue($congestionRelievedEventHasTakenPlace);
}
public function testItCreatesAWorkerForASubmittedTask(): void
{
$limit = $this->createMock(ConcurrencyLimit::class);
$loop = $this->createMock(LoopInterface::class);
$context = new Context($loop, $limit);
$limit->method('isReachedBy')->willReturn(false);
$loop->expects(self::once())->method('futureTick')->withConsecutive(
[fn() => []],
);
$context->submit(fn() => []);
}
public function testItRegistersMaintenanceCallbacksOnTheEventLoop(): void
{
$loop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class);
$loop->expects(self::exactly(2))->method('addPeriodicTimer')->withConsecutive(
[Context::CLEANUP_INTERVAL, fn() => []],
[Context::GC_INTERVAL, fn() => []]
);
new Context($loop, $limit);
}
public function testItProxiesWorkersEventsToSelf(): void
{
$loop = $this->createMock(LoopInterface::class);
$limit = $this->createMock(ConcurrencyLimit::class);
$workers = $this->createMock(Workers::class);
$workers->expects(self::atLeast(2))->method('on')->withConsecutive(
['worker_started', fn() => []],
['worker_stopped', fn() => []]
);
new Context($loop, $limit, $workers);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Toalett\Multiprocessing\Tests\Tools;
use ReflectionObject;
trait PropertyInspector
{
protected function getProperty(object $object, string $propertyName)
{
$reflector = new ReflectionObject($object);
$property = $reflector->getProperty($propertyName);
$property->setAccessible(true);
return $property->getValue($object);
}
}

41
src/Tests/WorkersTest.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
namespace Toalett\Multiprocessing\Tests;
use ReflectionObject;
use Toalett\Multiprocessing\Workers;
use PHPUnit\Framework\TestCase;
class WorkersTest extends TestCase
{
public function testItEmitsAnEventWhenAWorkerIsStarted(): void
{
$workers = new Workers();
$workerStartedEventHasTakenPlace = false;
$workers->on('worker_started', function() use (&$workerStartedEventHasTakenPlace) {
$workerStartedEventHasTakenPlace = true;
});
self::assertFalse($workerStartedEventHasTakenPlace);
$workers->createWorkerFor(fn() => exit(0), []);
self::assertTrue($workerStartedEventHasTakenPlace);
}
public function testItEmitsAnEventWhenAWorkerIsRemoved(): void
{
$workers = new Workers();
$reflector = new ReflectionObject($workers);
$method = $reflector->getMethod('remove');
$method->setAccessible(true);
$workerStoppedEventHasTakenPlace = false;
$workers->on('worker_stopped', function() use (&$workerStoppedEventHasTakenPlace) {
$workerStoppedEventHasTakenPlace = true;
});
self::assertFalse($workerStoppedEventHasTakenPlace);
$method->invoke($workers, 0);
self::assertTrue($workerStoppedEventHasTakenPlace);
}
}