Implement Unit Tests, add Readme, add examples, stronger implementation
This commit is contained in:
88
src/Tests/ConcurrencyLimitTest.php
Normal file
88
src/Tests/ConcurrencyLimitTest.php
Normal 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],
|
||||
];
|
||||
}
|
||||
}
|
||||
68
src/Tests/ContextBuilderTest.php
Normal file
68
src/Tests/ContextBuilderTest.php
Normal 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
98
src/Tests/ContextTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
16
src/Tests/Tools/PropertyInspector.php
Normal file
16
src/Tests/Tools/PropertyInspector.php
Normal 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
41
src/Tests/WorkersTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user