暂存
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Barrier;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
* Class FiberBarrierTest
|
||||
*
|
||||
* Tests for the Fiber Barrier implementation.
|
||||
*/
|
||||
class BarrierTest extends TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* Test that the barrier is set to null after calling wait.
|
||||
*/
|
||||
public function testWaitSetsBarrierToNull()
|
||||
{
|
||||
$barrier = Barrier::create();
|
||||
$results = [0];
|
||||
Coroutine::create(function () use ($barrier, &$results) {
|
||||
Timer::sleep(0.1);
|
||||
$results[] = 1;
|
||||
});
|
||||
Coroutine::create(function () use ($barrier, &$results) {
|
||||
Timer::sleep(0.2);
|
||||
$results[] = 2;
|
||||
});
|
||||
Coroutine::create(function () use ($barrier, &$results) {
|
||||
Timer::sleep(0.3);
|
||||
$results[] = 3;
|
||||
});
|
||||
Barrier::wait($barrier);
|
||||
$this->assertNull($barrier, 'Barrier should be null after wait is called.');
|
||||
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
|
||||
}
|
||||
}
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use stdClass;
|
||||
use Workerman\Coroutine\Channel;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Workerman\Coroutine\Channel\Memory;
|
||||
use Workerman\Coroutine;
|
||||
|
||||
class ChannelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test initializing channel with valid capacity.
|
||||
*/
|
||||
public function testInitializeWithValidCapacity()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$this->assertInstanceOf(Channel::class, $channel);
|
||||
$this->assertEquals(1, $channel->getCapacity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test initializing channel with invalid capacities.
|
||||
*/
|
||||
#[DataProvider('invalidCapacitiesProvider')]
|
||||
public function testInitializeWithInvalidCapacity($capacity)
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
new Channel($capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for invalid capacities.
|
||||
*/
|
||||
public static function invalidCapacitiesProvider(): array
|
||||
{
|
||||
return [
|
||||
[0],
|
||||
[-1],
|
||||
[-100]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing and popping data.
|
||||
*/
|
||||
public function testPushAndPop()
|
||||
{
|
||||
$channel = new Channel(2);
|
||||
$data1 = 'test data 1';
|
||||
$data2 = 'test data 2';
|
||||
|
||||
// Push data into the channel
|
||||
$this->assertTrue($channel->push($data1));
|
||||
$this->assertTrue($channel->push($data2));
|
||||
|
||||
// Verify the length of the channel
|
||||
$this->assertEquals(2, $channel->length());
|
||||
|
||||
// Pop data from the channel
|
||||
$this->assertEquals($data1, $channel->pop());
|
||||
$this->assertEquals($data2, $channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing data when the channel is full.
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testPushWhenFull()
|
||||
{
|
||||
// Memory driver does not support push with timeout
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$timeout = 0.5;
|
||||
// Attempt to push when the channel is full with a timeout
|
||||
$startTime = microtime(true);
|
||||
$this->assertFalse($channel->push('data2', $timeout));
|
||||
$elapsedTime = microtime(true) - $startTime;
|
||||
|
||||
// Verify that the push operation timed out
|
||||
$this->assertTrue(0.1 > abs($elapsedTime - $timeout));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test popping data when the channel is empty.
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testPopWhenEmpty()
|
||||
{
|
||||
// Memory driver does not support push with timeout
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
|
||||
// Attempt to pop when the channel is empty with a timeout
|
||||
$startTime = microtime(true);
|
||||
$this->assertFalse($channel->pop(0.1));
|
||||
$elapsedTime = microtime(true) - $startTime;
|
||||
|
||||
// Verify that the pop operation timed out
|
||||
$this->assertGreaterThanOrEqual(0.09, $elapsedTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test closing the channel and its effects.
|
||||
*/
|
||||
public function testCloseChannel()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$this->assertTrue($channel->push('data'));
|
||||
|
||||
// Close the channel
|
||||
$channel->close();
|
||||
|
||||
// Attempt to push after closing
|
||||
$this->assertFalse($channel->push('new data'));
|
||||
|
||||
// Pop the remaining data
|
||||
$this->assertEquals('data', $channel->pop());
|
||||
|
||||
// Attempt to pop after channel is empty and closed
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that push and pop return false when channel is closed.
|
||||
*/
|
||||
public function testPushAndPopReturnFalseWhenClosed()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$channel->close();
|
||||
|
||||
$this->assertFalse($channel->push('data'));
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the length and capacity methods.
|
||||
*/
|
||||
public function testLengthAndCapacity()
|
||||
{
|
||||
$channel = new Channel(5);
|
||||
$this->assertEquals(0, $channel->length());
|
||||
$this->assertEquals(5, $channel->getCapacity());
|
||||
|
||||
$channel->push('data1');
|
||||
$channel->push('data2');
|
||||
|
||||
$this->assertEquals(2, $channel->length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing and popping with different data types.
|
||||
*/
|
||||
#[DataProvider('dataTypesProvider')]
|
||||
public function testPushAndPopWithDifferentDataTypes($data)
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$this->assertTrue($channel->push($data));
|
||||
$this->assertSame($data, $channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for different data types.
|
||||
*/
|
||||
public static function dataTypesProvider(): array
|
||||
{
|
||||
return [
|
||||
['string'],
|
||||
[123],
|
||||
[123.456],
|
||||
[true],
|
||||
[false],
|
||||
[null],
|
||||
[[]],
|
||||
[['key' => 'value']],
|
||||
[new stdClass()],
|
||||
[fopen('php://memory', 'r')],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing to a closed channel immediately returns false.
|
||||
*/
|
||||
public function testPushToClosedChannel()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$channel->close();
|
||||
$this->assertFalse($channel->push('data', 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test popping from a closed and empty channel immediately returns false.
|
||||
*/
|
||||
public function testPopFromClosedAndEmptyChannel()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$channel->close();
|
||||
$this->assertFalse($channel->pop(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
protected function driverIsMemory(): bool
|
||||
{
|
||||
$reflectionClass = new ReflectionClass(Channel::class);
|
||||
$instance = $reflectionClass->newInstance();
|
||||
$property = $reflectionClass->getProperty('driver');
|
||||
$driverValue = $property->getValue($instance);
|
||||
return $driverValue instanceof Memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasConsumers 当没有消费者时返回 false
|
||||
*/
|
||||
public function testHasConsumersWhenNoConsumers()
|
||||
{
|
||||
if (!Coroutine::isCoroutine()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$this->assertFalse($channel->hasConsumers());
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasConsumers 当有消费者等待时返回 true
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testHasConsumersWhenConsumersWaiting()
|
||||
{
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$sync = new Channel(1);
|
||||
|
||||
Coroutine::create(function () use ($channel, $sync) {
|
||||
$sync->push(true);
|
||||
$channel->pop();
|
||||
});
|
||||
|
||||
$sync->pop();
|
||||
|
||||
$this->assertTrue($channel->hasConsumers());
|
||||
|
||||
Coroutine::create(function () use ($channel) {
|
||||
$channel->push('data');
|
||||
});
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasProducers 当没有生产者时返回 false
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testHasProducersWhenNoProducers()
|
||||
{
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$this->assertFalse($channel->hasProducers());
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasProducers 当有生产者等待时返回 true
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testHasProducersWhenProducersWaiting()
|
||||
{
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$channel->push('data1');
|
||||
|
||||
$sync = new Channel(1);
|
||||
|
||||
Coroutine::create(function () use ($channel, $sync) {
|
||||
$sync->push(true);
|
||||
$channel->push('data2');
|
||||
});
|
||||
|
||||
$sync->pop();
|
||||
|
||||
$this->assertTrue($channel->hasProducers());
|
||||
|
||||
$channel->pop();
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
use ArrayObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Context;
|
||||
use Workerman\Coroutine;
|
||||
|
||||
// Now, the test cases
|
||||
class ContextTest extends TestCase
|
||||
{
|
||||
public function testContextSetAndGetWithinCoroutine()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
$key = 'testContextSetAndGetWithinCoroutine';
|
||||
Context::set($key, 'value');
|
||||
$this->assertEquals('value', Context::get($key));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextGet()
|
||||
{
|
||||
Context::reset(new ArrayObject(['not_exist' => 'value']));
|
||||
$key = 'testContextGet';
|
||||
Context::reset(new ArrayObject([$key => 'value']));
|
||||
$context = Context::get();
|
||||
$this->assertArrayNotHasKey('not_exist', $context);
|
||||
$this->assertObjectNotHasProperty('not_exist', $context);
|
||||
$this->assertArrayHasKey($key, $context);
|
||||
$this->assertObjectHasProperty($key, $context);
|
||||
$this->assertEquals('value', $context[$key]);
|
||||
$this->assertEquals('value', $context->$key);
|
||||
$this->assertInstanceOf('ArrayObject', $context);
|
||||
unset($context[$key]);
|
||||
$this->assertNull(Context::get($key));
|
||||
$context[$key] = 'value';
|
||||
$this->assertEquals('value', Context::get($key));
|
||||
unset($context->$key);
|
||||
$this->assertNull(Context::get($key));
|
||||
$context->$key = 'value';
|
||||
$this->assertEquals('value', Context::get($key));
|
||||
}
|
||||
|
||||
public function testContextIsolationBetweenCoroutines()
|
||||
{
|
||||
$values = [];
|
||||
|
||||
Coroutine::create(function () use (&$values) {
|
||||
Context::set('key', 'value1');
|
||||
$values[] = Context::get('key');
|
||||
// Ensure the value is not available after coroutine ends
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
Coroutine::create(function () use (&$values) {
|
||||
Context::set('key', 'value2');
|
||||
$values[] = Context::get('key');
|
||||
// Ensure the value is not available after coroutine ends
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
$this->assertEquals(['value1', 'value2'], $values);
|
||||
}
|
||||
|
||||
public function testContextDestroyedAfterCoroutineEnds()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key', 'value');
|
||||
$this->assertTrue(Context::has('key'));
|
||||
// Simulate coroutine end and context destruction
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
// After coroutine ends, the context should be destroyed
|
||||
// Need to simulate this by trying to access context outside coroutine
|
||||
$this->assertNull(Context::get('key'));
|
||||
$this->assertFalse(Context::has('key'));
|
||||
}
|
||||
|
||||
public function testContextHasMethod()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
$this->assertFalse(Context::has('key'));
|
||||
Context::set('key', 'value');
|
||||
$this->assertTrue(Context::has('key'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextResetMethod()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::reset(new ArrayObject(['key3' => 'value1']));
|
||||
Context::reset(new ArrayObject(['key1' => 'value1', 'key2' => 'value2']));
|
||||
$this->assertEquals('value1', Context::get('key1'));
|
||||
$this->assertEquals('value2', Context::get('key2'));
|
||||
// Test that other keys are not set
|
||||
$this->assertNull(Context::get('key3'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextDataNotSharedBetweenCoroutines()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
Coroutine::create(function () use (&$result) {
|
||||
Context::set('counter', 1);
|
||||
$result[] = Context::get('counter');
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
Coroutine::create(function () use (&$result) {
|
||||
$this->assertNull(Context::get('counter'));
|
||||
Context::set('counter', 2);
|
||||
$result[] = Context::get('counter');
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
$this->assertEquals([1, 2], $result);
|
||||
}
|
||||
|
||||
public function testContextDefaultValues()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
$this->assertEquals('default', Context::get('non_existing_key', 'default'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextSetOverrideValue()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key', 'initial');
|
||||
$this->assertEquals('initial', Context::get('key'));
|
||||
Context::set('key', 'overridden');
|
||||
$this->assertEquals('overridden', Context::get('key'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextMultipleKeys()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key1', 'value1');
|
||||
Context::set('key2', 'value2');
|
||||
$this->assertEquals('value1', Context::get('key1'));
|
||||
$this->assertEquals('value2', Context::get('key2'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextPersistenceWithinCoroutine()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key', 'value');
|
||||
|
||||
// Simulate asynchronous operation within coroutine
|
||||
$this->someAsyncOperation(function () {
|
||||
$this->assertEquals('value', Context::get('key'));
|
||||
});
|
||||
|
||||
// Context should persist throughout the coroutine
|
||||
$this->assertEquals('value', Context::get('key'));
|
||||
});
|
||||
}
|
||||
|
||||
private function someAsyncOperation(callable $callback)
|
||||
{
|
||||
// Simulate async operation
|
||||
$callback();
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Coroutine\Coroutine\CoroutineInterface;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Worker;
|
||||
|
||||
class CoroutineTest extends TestCase
|
||||
{
|
||||
public function testCreateReturnsCoroutineInterface()
|
||||
{
|
||||
$callable = function() {};
|
||||
$coroutine = Coroutine::create($callable);
|
||||
$this->assertInstanceOf(CoroutineInterface::class, $coroutine);
|
||||
}
|
||||
|
||||
public function testStartExecutesCoroutine()
|
||||
{
|
||||
$value = null;
|
||||
Coroutine::create(function() use (&$value) {
|
||||
$value = 'started';
|
||||
});
|
||||
$this->assertEquals('started', $value);
|
||||
}
|
||||
|
||||
public function testSuspendAndResumeCoroutine()
|
||||
{
|
||||
if (Worker::$eventLoopClass === Swoole::class) {
|
||||
// Swoole does not support suspend and resume
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$value = [];
|
||||
$coroutine = Coroutine::create(function() use (&$value) {
|
||||
$value[] = 'before suspend';
|
||||
$resumedValue = Coroutine::suspend();
|
||||
$value[] = 'after resume';
|
||||
$value[] = $resumedValue;
|
||||
});
|
||||
$this->assertEquals(['before suspend'], $value);
|
||||
$coroutine->resume('resumed data');
|
||||
unset($coroutine);
|
||||
gc_collect_cycles();
|
||||
$this->assertEquals(['before suspend', 'after resume', 'resumed data'], $value);
|
||||
}
|
||||
|
||||
public function testGetCurrentReturnsCurrentCoroutine()
|
||||
{
|
||||
$currentCoroutine = null;
|
||||
$coroutine = Coroutine::create(function() use (&$currentCoroutine) {
|
||||
$currentCoroutine = Coroutine::getCurrent();
|
||||
});
|
||||
$this->assertSame($coroutine, $currentCoroutine);
|
||||
}
|
||||
|
||||
public function testCoroutineIdIsInteger()
|
||||
{
|
||||
$coroutine = Coroutine::create(function() {});
|
||||
$id = $coroutine->id();
|
||||
$this->assertIsInt($id);
|
||||
}
|
||||
|
||||
public function testDeferExecutesAfterCoroutineDestruction()
|
||||
{
|
||||
$value = [];
|
||||
$coroutine = Coroutine::create(function() use (&$value) {
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer1';
|
||||
});
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer2';
|
||||
});
|
||||
$value[] = 'before suspend';
|
||||
Coroutine::suspend();
|
||||
$value[] = 'after resume';
|
||||
});
|
||||
$this->assertEquals(['before suspend'], $value);
|
||||
$coroutine->resume();
|
||||
unset($coroutine);
|
||||
gc_collect_cycles();
|
||||
$this->assertEquals(['before suspend', 'after resume', 'defer2', 'defer1'], $value);
|
||||
}
|
||||
|
||||
public function testMultipleCoroutines()
|
||||
{
|
||||
$sequence = [];
|
||||
$coroutine1 = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'coroutine1 start';
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'coroutine1 resumed';
|
||||
});
|
||||
$coroutine2 = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'coroutine2 start';
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'coroutine2 resumed';
|
||||
});
|
||||
$this->assertEquals(['coroutine1 start', 'coroutine2 start'], $sequence);
|
||||
$coroutine1->resume();
|
||||
$coroutine2->resume();
|
||||
$this->assertEquals(
|
||||
['coroutine1 start', 'coroutine2 start', 'coroutine1 resumed', 'coroutine2 resumed'],
|
||||
$sequence
|
||||
);
|
||||
}
|
||||
|
||||
public function testCoroutineWithArguments()
|
||||
{
|
||||
$result = null;
|
||||
$coroutine = new Coroutine(function($a, $b) use (&$result) {
|
||||
$result = $a + $b;
|
||||
});
|
||||
$coroutine->start(2, 3);
|
||||
$this->assertEquals(5, $result);
|
||||
}
|
||||
|
||||
public function testSuspendReturnsValue()
|
||||
{
|
||||
if (Worker::$eventLoopClass === Swoole::class) {
|
||||
// Swoole does not support suspend and resume
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$coroutine = new Coroutine(function() {
|
||||
$valueFromResume = Coroutine::suspend('first suspend');
|
||||
Coroutine::suspend($valueFromResume);
|
||||
});
|
||||
$first_suspend = $coroutine->start();
|
||||
$this->assertEquals('first suspend', $first_suspend);
|
||||
$result = $coroutine->resume('value from resume');
|
||||
$this->assertEquals('value from resume', $result);
|
||||
}
|
||||
|
||||
public function testNestedCoroutines()
|
||||
{
|
||||
$sequence = [];
|
||||
$coroutine = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'outer start';
|
||||
$inner = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'inner start';
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'inner resumed';
|
||||
});
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'outer resumed';
|
||||
$inner->resume();
|
||||
$sequence[] = 'outer end';
|
||||
});
|
||||
$this->assertEquals(['outer start', 'inner start'], $sequence);
|
||||
$coroutine->resume();
|
||||
$this->assertEquals(['outer start', 'inner start', 'outer resumed', 'inner resumed', 'outer end'], $sequence);
|
||||
}
|
||||
|
||||
/*public function testCoroutineExceptionHandling()
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Test exception');
|
||||
Coroutine::create(function() {
|
||||
throw new \Exception('Test exception');
|
||||
});
|
||||
}*/
|
||||
|
||||
public function testDeferOrder()
|
||||
{
|
||||
$value = [];
|
||||
$coroutine = Coroutine::create(function() use (&$value) {
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer1';
|
||||
});
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer2';
|
||||
});
|
||||
$value[] = 'coroutine body';
|
||||
});
|
||||
unset($coroutine);
|
||||
// Force garbage collection
|
||||
gc_collect_cycles();
|
||||
$this->assertEquals(['coroutine body', 'defer2', 'defer1'], $value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Channel\Fiber as Channel;
|
||||
use Workerman\Timer;
|
||||
use Fiber as BaseFiber;
|
||||
|
||||
class FiberChannelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test basic push and pop operations.
|
||||
*/
|
||||
public function testBasicPushPop()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$fiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('test data');
|
||||
});
|
||||
|
||||
$fiber->start();
|
||||
|
||||
$this->assertEquals('test data', $channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that pop will block until data is available or timeout occurs.
|
||||
*/
|
||||
public function testPopWithTimeout()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$fiber = new BaseFiber(function() use ($channel) {
|
||||
$result = $channel->pop(0.5);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
$fiber->start();
|
||||
|
||||
// Allow time for the fiber to suspend and wait
|
||||
Timer::sleep(0.2); // 200 ms
|
||||
|
||||
// Ensure that the fiber is still waiting (not timed out yet)
|
||||
$this->assertTrue($fiber->isSuspended());
|
||||
|
||||
// Wait until the timeout should have occurred
|
||||
Timer::sleep(0.4); // 400 ms
|
||||
|
||||
$endTime = microtime(true);
|
||||
|
||||
$this->assertTrue($fiber->isTerminated());
|
||||
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that push will block when capacity is reached and timeout occurs.
|
||||
*/
|
||||
public function testPushWithTimeout()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$fiber = new BaseFiber(function() use ($channel) {
|
||||
$result = $channel->push('data2', 0.5);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
$fiber->start();
|
||||
|
||||
// Allow time for the fiber to suspend and wait
|
||||
Timer::sleep(0.2); // 200 ms
|
||||
|
||||
// Ensure that the fiber is still waiting (not timed out yet)
|
||||
$this->assertTrue($fiber->isSuspended());
|
||||
|
||||
// Wait until the timeout should have occurred
|
||||
Timer::sleep(0.4); // 400 ms
|
||||
|
||||
$endTime = microtime(true);
|
||||
|
||||
$this->assertTrue($fiber->isTerminated());
|
||||
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that push returns false immediately if capacity is full and timeout is zero.
|
||||
*/
|
||||
public function testPushNonBlockingWhenFull()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$result = $channel->push('data2', 0);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that pop returns false immediately if the channel is empty and timeout is zero.
|
||||
*/
|
||||
public function testPopNonBlockingWhenEmpty()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$result = $channel->pop(0);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test closing the channel.
|
||||
*/
|
||||
public function testCloseChannel()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$channel->close();
|
||||
|
||||
$this->assertFalse($channel->push('data'));
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that waiting pushers and poppers are resumed when the channel is closed.
|
||||
*/
|
||||
public function testWaitersAreResumedOnClose()
|
||||
{
|
||||
$channelPush = new Channel(1);
|
||||
$channelPop = new Channel(1);
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channelPush) {
|
||||
$channelPush->push('data', 1);
|
||||
$result = $channelPush->push('data', 1);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channelPop) {
|
||||
$result = $channelPop->pop(1);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$pushFiber->start();
|
||||
$popFiber->start();
|
||||
|
||||
// Allow time for fibers to suspend
|
||||
Timer::sleep(0.1); // 100 ms
|
||||
|
||||
// Close the channel to resume fibers
|
||||
$channelPush->close();
|
||||
$channelPop->close();
|
||||
|
||||
// Allow time for fibers to process after resuming
|
||||
Timer::sleep(0.1); // 100 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that length and getCapacity methods return correct values.
|
||||
*/
|
||||
public function testLengthAndCapacity()
|
||||
{
|
||||
$capacity = 2;
|
||||
$channel = new Channel($capacity);
|
||||
|
||||
$this->assertEquals(0, $channel->length());
|
||||
$this->assertEquals($capacity, $channel->getCapacity());
|
||||
|
||||
$channel->push('data1');
|
||||
$this->assertEquals(1, $channel->length());
|
||||
|
||||
$channel->push('data2');
|
||||
$this->assertEquals(2, $channel->length());
|
||||
|
||||
$channel->pop();
|
||||
$this->assertEquals(1, $channel->length());
|
||||
|
||||
$channel->pop();
|
||||
$this->assertEquals(0, $channel->length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing to a closed channel.
|
||||
*/
|
||||
public function testPushToClosedChannel()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$channel->close();
|
||||
|
||||
$result = $channel->push('data');
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test popping from a closed channel.
|
||||
*/
|
||||
public function testPopFromClosedChannel()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$channel->push('data');
|
||||
|
||||
$channel->close();
|
||||
|
||||
$this->assertEquals('data', $channel->pop());
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test multiple push and pop operations with fibers.
|
||||
*/
|
||||
public function testMultiplePushPopWithFibers()
|
||||
{
|
||||
$channel = new Channel(2);
|
||||
|
||||
$results = [];
|
||||
|
||||
$producerFiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('data1');
|
||||
$channel->push('data2');
|
||||
$channel->push('data3');
|
||||
});
|
||||
|
||||
$consumerFiber = new BaseFiber(function() use ($channel, &$results) {
|
||||
$results[] = $channel->pop();
|
||||
$results[] = $channel->pop();
|
||||
$results[] = $channel->pop();
|
||||
});
|
||||
|
||||
$producerFiber->start();
|
||||
$consumerFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
usleep(500000); // 500 ms
|
||||
|
||||
$this->assertEquals(['data1', 'data2', 'data3'], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that fibers are properly blocked and resumed in push and pop operations.
|
||||
*/
|
||||
public function testFiberBlockingAndResuming()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('data1');
|
||||
$channel->push('data2');
|
||||
$channel->push('data3');
|
||||
});
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channel) {
|
||||
$this->assertEquals('data1', $channel->pop());
|
||||
$this->assertEquals('data2', $channel->pop());
|
||||
$this->assertEquals('data3', $channel->pop());
|
||||
});
|
||||
|
||||
$pushFiber->start();
|
||||
$popFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
Timer::sleep(0.5); // 500 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that pushing data after capacity is reached blocks until space is available.
|
||||
*/
|
||||
public function testPushBlocksWhenFull()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$channel->push('data1');
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('data2');
|
||||
});
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channel) {
|
||||
Timer::sleep(0.2); // Wait before popping
|
||||
$this->assertEquals('data1', $channel->pop());
|
||||
});
|
||||
|
||||
$pushFiber->start();
|
||||
$popFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
Timer::sleep(0.5); // 500 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that popping data from an empty channel blocks until data is available.
|
||||
*/
|
||||
public function testPopBlocksWhenEmpty()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channel) {
|
||||
$this->assertEquals('data1', $channel->pop());
|
||||
});
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channel) {
|
||||
Timer::sleep(0.2); // Wait before pushing
|
||||
$channel->push('data1');
|
||||
});
|
||||
|
||||
$popFiber->start();
|
||||
$pushFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
Timer::sleep(0.5); // 500 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing and popping with zero timeout.
|
||||
*/
|
||||
public function testPushPopWithZeroTimeout()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$result = $channel->push('data2', 0);
|
||||
$this->assertFalse($result);
|
||||
|
||||
$result = $channel->pop(0);
|
||||
$this->assertEquals('data1', $result);
|
||||
|
||||
$result = $channel->pop(0);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Locker;
|
||||
use RuntimeException;
|
||||
use Workerman\Coroutine;
|
||||
use ReflectionClass;
|
||||
use Workerman\Timer;
|
||||
|
||||
class LockerTest extends TestCase
|
||||
{
|
||||
|
||||
public function testLock()
|
||||
{
|
||||
$key = 'testLock';
|
||||
Locker::lock($key);
|
||||
$timeStart = microtime(true);
|
||||
$timeDiff2 = 0;
|
||||
Coroutine::create(function () use ($key, $timeStart, &$timeDiff2) {
|
||||
$this->assertChannelExists($key);
|
||||
Locker::lock($key);
|
||||
$timeDiff = microtime(true) - $timeStart;
|
||||
$this->assertGreaterThan($timeDiff2, $timeDiff);
|
||||
Locker::unlock($key);
|
||||
});
|
||||
usleep(100000);
|
||||
$timeDiff2 = microtime(true) - $timeStart;
|
||||
Locker::unlock($key);
|
||||
}
|
||||
|
||||
public function testLockAndUnlock()
|
||||
{
|
||||
$key = 'testLockAndUnlock';
|
||||
$this->assertTrue(Locker::lock($key));
|
||||
$this->assertTrue(Locker::unlock($key));
|
||||
$this->assertChannelRemoved($key);
|
||||
}
|
||||
|
||||
public function testUnlockWithoutLockThrowsException()
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
Locker::unlock('non_existent_key');
|
||||
}
|
||||
|
||||
public function testRelockAfterUnlock()
|
||||
{
|
||||
$key = 'testRelockAfterUnlock';
|
||||
Locker::lock($key);
|
||||
Locker::unlock($key);
|
||||
|
||||
$this->assertTrue(Locker::lock($key));
|
||||
Locker::unlock($key);
|
||||
$this->assertChannelRemoved($key);
|
||||
}
|
||||
|
||||
public function testMultipleCoroutinesLocking()
|
||||
{
|
||||
$key = 'testMultipleCoroutinesLocking';
|
||||
$results = [];
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Locker::lock($key);
|
||||
$results[] = 'A';
|
||||
Timer::sleep(0.1);
|
||||
usleep(100000);
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Timer::sleep(0.05);
|
||||
Locker::lock($key);
|
||||
$results[] = 'B';
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Timer::sleep(0.05);
|
||||
Locker::lock($key);
|
||||
$results[] = 'C';
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Timer::sleep(0.3);
|
||||
$this->assertEquals(['A', 'B', 'C'], $results);
|
||||
$this->assertChannelRemoved($key);
|
||||
}
|
||||
|
||||
public function testChannelRemainsWhenWaiting()
|
||||
{
|
||||
$key = 'testChannelRemainsWhenWaiting';
|
||||
Locker::lock($key);
|
||||
|
||||
Coroutine::create(function () use ($key) {
|
||||
Coroutine::create(function () use ($key) {
|
||||
Locker::lock($key);
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
Locker::unlock($key);
|
||||
|
||||
$this->assertChannelRemoved($key);
|
||||
});
|
||||
}
|
||||
|
||||
private function assertChannelExists(string $key): void
|
||||
{
|
||||
$channels = $this->getChannels();
|
||||
$this->assertArrayHasKey($key, $channels, "Channel for key '$key' should exist");
|
||||
}
|
||||
|
||||
private function assertChannelRemoved(string $key): void
|
||||
{
|
||||
$channels = $this->getChannels();
|
||||
$this->assertArrayNotHasKey($key, $channels, "Channel for key '$key' should be removed");
|
||||
}
|
||||
|
||||
private function getChannels(): array
|
||||
{
|
||||
$reflector = new ReflectionClass(Locker::class);
|
||||
$property = $reflector->getProperty('channels');
|
||||
return $property->getValue();
|
||||
}
|
||||
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Parallel;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
* Test cases for the Workerman\Coroutine\Parallel class.
|
||||
*/
|
||||
class ParallelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test that callables are added and executed, and results are collected properly.
|
||||
*/
|
||||
public function testAddAndWait()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
// Simulate some work.
|
||||
Timer::sleep(0.01);
|
||||
return 1;
|
||||
}, 'task1');
|
||||
|
||||
$parallel->add(function () {
|
||||
// Simulate some work.
|
||||
Timer::sleep(0.005);
|
||||
return 2;
|
||||
}, 'task2');
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
$this->assertEquals(['task1' => 1, 'task2' => 2], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that exceptions thrown in callables are caught and can be retrieved.
|
||||
*/
|
||||
public function testExceptions()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
throw new \Exception('Test exception');
|
||||
}, 'task_with_exception');
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'normal result';
|
||||
}, 'normal_task');
|
||||
|
||||
$results = $parallel->wait();
|
||||
$exceptions = $parallel->getExceptions();
|
||||
|
||||
// Check that the normal task result is present.
|
||||
$this->assertEquals(['normal_task' => 'normal result'], $results);
|
||||
|
||||
// Check that the exception is captured for the failing task.
|
||||
$this->assertArrayHasKey('task_with_exception', $exceptions);
|
||||
$this->assertInstanceOf(\Exception::class, $exceptions['task_with_exception']);
|
||||
$this->assertEquals('Test exception', $exceptions['task_with_exception']->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test concurrency control by limiting the number of concurrent tasks.
|
||||
*/
|
||||
public function testConcurrencyLimit()
|
||||
{
|
||||
$concurrentLimit = 2;
|
||||
$parallel = new Parallel($concurrentLimit);
|
||||
|
||||
$startTimes = [];
|
||||
$endTimes = [];
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$parallel->add(function () use (&$startTimes, &$endTimes, $i) {
|
||||
$startTimes[$i] = microtime(true);
|
||||
// Simulate some work.
|
||||
Timer::sleep(0.1); // 100 milliseconds
|
||||
$endTimes[$i] = microtime(true);
|
||||
return $i;
|
||||
}, "task{$i}");
|
||||
}
|
||||
|
||||
$parallel->wait();
|
||||
|
||||
// Since we limited concurrency to 2, tasks should finish in batches.
|
||||
// We'll check that at no point more than $concurrentLimit tasks were running simultaneously.
|
||||
|
||||
// Collect start and end times into an array of intervals.
|
||||
$intervals = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$intervals[] = ['start' => $startTimes[$i], 'end' => $endTimes[$i]];
|
||||
}
|
||||
|
||||
// Check the maximum number of overlapping intervals does not exceed the concurrency limit.
|
||||
$maxConcurrent = $this->getMaxConcurrentIntervals($intervals);
|
||||
|
||||
$this->assertLessThanOrEqual($concurrentLimit, $maxConcurrent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine the maximum number of overlapping intervals.
|
||||
*
|
||||
* @param array $intervals
|
||||
* @return int
|
||||
*/
|
||||
private function getMaxConcurrentIntervals(array $intervals)
|
||||
{
|
||||
$events = [];
|
||||
foreach ($intervals as $interval) {
|
||||
$events[] = ['time' => $interval['start'], 'type' => 'start'];
|
||||
$events[] = ['time' => $interval['end'], 'type' => 'end'];
|
||||
}
|
||||
|
||||
// Sort events by time, 'start' before 'end' if times are equal.
|
||||
usort($events, function ($a, $b) {
|
||||
if ($a['time'] == $b['time']) {
|
||||
return $a['type'] === 'start' ? -1 : 1;
|
||||
}
|
||||
return $a['time'] < $b['time'] ? -1 : 1;
|
||||
});
|
||||
|
||||
$maxConcurrent = 0;
|
||||
$currentConcurrent = 0;
|
||||
|
||||
foreach ($events as $event) {
|
||||
if ($event['type'] === 'start') {
|
||||
$currentConcurrent++;
|
||||
if ($currentConcurrent > $maxConcurrent) {
|
||||
$maxConcurrent = $currentConcurrent;
|
||||
}
|
||||
} else {
|
||||
$currentConcurrent--;
|
||||
}
|
||||
}
|
||||
|
||||
return $maxConcurrent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that callables are executed in parallel when no concurrency limit is set.
|
||||
*/
|
||||
public function testParallelExecutionWithoutConcurrencyLimit()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$startTimes = [];
|
||||
$endTimes = [];
|
||||
|
||||
$parallel->add(function () use (&$startTimes, &$endTimes) {
|
||||
$startTimes[] = microtime(true);
|
||||
Timer::sleep(0.1); // 100 milliseconds
|
||||
$endTimes[] = microtime(true);
|
||||
return 'task1';
|
||||
}, 'task1');
|
||||
|
||||
$parallel->add(function () use (&$startTimes, &$endTimes) {
|
||||
$startTimes[] = microtime(true);
|
||||
Timer::sleep(0.1);// 100 milliseconds
|
||||
$endTimes[] = microtime(true);
|
||||
return 'task2';
|
||||
}, 'task2');
|
||||
|
||||
$parallel->wait();
|
||||
|
||||
// Calculate total elapsed time.
|
||||
$totalTime = max($endTimes) - min($startTimes);
|
||||
|
||||
// The total time should be approximately the duration of one task, not the sum of both.
|
||||
$this->assertLessThan(0.2, $totalTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding callables without specifying keys and ensure results are correctly indexed.
|
||||
*/
|
||||
public function testAddWithoutKeys()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'result1';
|
||||
});
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'result2';
|
||||
});
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
// Since no keys were specified, indices should be 0 and 1.
|
||||
$this->assertEquals(['result1', 'result2'], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the Parallel class can handle a large number of tasks.
|
||||
*/
|
||||
public function testLargeNumberOfTasks()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$taskCount = 100;
|
||||
for ($i = 0; $i < $taskCount; $i++) {
|
||||
$parallel->add(function () use ($i) {
|
||||
return $i * $i;
|
||||
}, "task{$i}");
|
||||
}
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
// Verify that all tasks have been completed and results are correct.
|
||||
for ($i = 0; $i < $taskCount; $i++) {
|
||||
$this->assertEquals($i * $i, $results["task{$i}"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that adding a non-callable throws a TypeError.
|
||||
*/
|
||||
public function testAddNonCallable()
|
||||
{
|
||||
$this->expectException(\TypeError::class);
|
||||
|
||||
$parallel = new Parallel();
|
||||
$parallel->add('not a callable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the wait method can be called multiple times safely.
|
||||
*/
|
||||
public function testMultipleWaitCalls()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'first call';
|
||||
}, 'task1');
|
||||
|
||||
$resultsFirst = $parallel->wait();
|
||||
|
||||
$this->assertEquals(['task1' => 'first call'], $resultsFirst);
|
||||
|
||||
// Add another task after first wait.
|
||||
$parallel->add(function () {
|
||||
return 'second call';
|
||||
}, 'task2');
|
||||
|
||||
$resultsSecond = $parallel->wait();
|
||||
|
||||
// Since the callbacks array is not cleared after wait, results should include both tasks.
|
||||
$this->assertEquals(['task1' => 'first call', 'task2' => 'second call'], $resultsSecond);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the class properly handles empty tasks (no callables added).
|
||||
*/
|
||||
public function testNoTasks()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the class handles tasks that return null.
|
||||
*/
|
||||
public function testTasksReturningNull()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
// No return statement, implicitly returns null.
|
||||
}, 'nullTask');
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
$this->assertArrayHasKey('nullTask', $results);
|
||||
$this->assertNull($results['nullTask']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test defer can be used in tasks.
|
||||
*/
|
||||
public function testWithDefer()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
$results = [];
|
||||
$parallel->add(function () use (&$results) {
|
||||
Coroutine::defer(function () use (&$results) {
|
||||
$results[] = 'defer1';
|
||||
});
|
||||
});
|
||||
$parallel->wait();
|
||||
$this->assertEquals(['defer1'], $results);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace test;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Coroutine\Exception\PoolException;
|
||||
use Workerman\Coroutine\Pool;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ReflectionClass;
|
||||
use stdClass;
|
||||
use Exception;
|
||||
use Workerman\Events\Event;
|
||||
use Workerman\Events\Select;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
class PoolTest extends TestCase
|
||||
{
|
||||
|
||||
public function testConstructorWithConfig()
|
||||
{
|
||||
$config = [
|
||||
'min_connections' => 2,
|
||||
'idle_timeout' => 30,
|
||||
'heartbeat_interval' => 10,
|
||||
'wait_timeout' => 5,
|
||||
];
|
||||
$pool = new Pool(10, $config);
|
||||
|
||||
$this->assertEquals(10, $this->getPrivateProperty($pool, 'maxConnections'));
|
||||
$this->assertEquals(2, $this->getPrivateProperty($pool, 'minConnections'));
|
||||
$this->assertEquals(30, $this->getPrivateProperty($pool, 'idleTimeout'));
|
||||
$this->assertEquals(10, $this->getPrivateProperty($pool, 'heartbeatInterval'));
|
||||
$this->assertEquals(5, $this->getPrivateProperty($pool, 'waitTimeout'));
|
||||
}
|
||||
|
||||
public function testSetConnectionCreator()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
$connectionCreator = function () {
|
||||
return new stdClass();
|
||||
};
|
||||
$pool->setConnectionCreator($connectionCreator);
|
||||
$this->assertSame($connectionCreator, $this->getPrivateProperty($pool, 'connectionCreateHandler'));
|
||||
}
|
||||
|
||||
public function testSetConnectionCloser()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
$connectionCloser = function ($conn) {
|
||||
// Close connection.
|
||||
};
|
||||
$pool->setConnectionCloser($connectionCloser);
|
||||
$this->assertSame($connectionCloser, $this->getPrivateProperty($pool, 'connectionDestroyHandler'));
|
||||
}
|
||||
|
||||
public function testGetConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connectionMock = $this->createMock(stdClass::class);
|
||||
|
||||
// 设置连接创建器
|
||||
$pool->setConnectionCreator(function () use ($connectionMock) {
|
||||
return $connectionMock;
|
||||
});
|
||||
|
||||
$connection = $pool->get();
|
||||
|
||||
$this->assertSame($connectionMock, $connection);
|
||||
$this->assertEquals(1, $this->getCurrentConnections($pool));
|
||||
|
||||
// 检查 WeakMap 是否更新
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
|
||||
$this->assertTrue($connections->offsetExists($connection));
|
||||
$this->assertTrue($lastUsedTimes->offsetExists($connection));
|
||||
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testPutConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connectionMock = $this->createMock(stdClass::class);
|
||||
|
||||
$pool->setConnectionCreator(function () use ($connectionMock) {
|
||||
return $connectionMock;
|
||||
});
|
||||
|
||||
$connection = $pool->get();
|
||||
|
||||
$pool->put($connection);
|
||||
|
||||
if (Coroutine::isCoroutine()) {
|
||||
$channel = $this->getPrivateProperty($pool, 'channel');
|
||||
$this->assertEquals(1, $channel->length());
|
||||
}
|
||||
|
||||
$this->assertEquals(1, $pool->getConnectionCount());
|
||||
}
|
||||
|
||||
public function testPutConnectionDoesNotBelong()
|
||||
{
|
||||
$this->expectException(PoolException::class);
|
||||
$this->expectExceptionMessage('The connection does not belong to the connection pool.');
|
||||
|
||||
$pool = new Pool(5);
|
||||
$connection = new stdClass();
|
||||
|
||||
$pool->put($connection);
|
||||
}
|
||||
|
||||
public function testCreateConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
$connectionMock = $this->createMock(stdClass::class);
|
||||
|
||||
$pool->setConnectionCreator(function () use ($connectionMock) {
|
||||
return $connectionMock;
|
||||
});
|
||||
|
||||
$connection = $pool->createConnection();
|
||||
|
||||
$this->assertSame($connectionMock, $connection);
|
||||
|
||||
// 确保 currentConnections 增加
|
||||
$this->assertEquals(1, $this->getCurrentConnections($pool));
|
||||
|
||||
// 检查 WeakMap 是否更新
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
|
||||
$this->assertTrue($connections->offsetExists($connection));
|
||||
$this->assertTrue($lastUsedTimes->offsetExists($connection));
|
||||
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testCreateMaxConnections()
|
||||
{
|
||||
if (in_array(Worker::$eventLoopClass, [Select::class, Event::class])) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$maxConnections = 2;
|
||||
$pool = new Pool($maxConnections);
|
||||
|
||||
$pool->setConnectionCreator(function () {
|
||||
Timer::sleep(0.01);
|
||||
return $this->createMock(stdClass::class);
|
||||
});
|
||||
|
||||
$connections = [];
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
Coroutine::create(function () use ($pool, &$connections) {
|
||||
$connections[] = $pool->get();
|
||||
});
|
||||
}
|
||||
|
||||
Timer::sleep(0.1);
|
||||
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
|
||||
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
|
||||
$this->assertCount($maxConnections, $lastUsedTimes);
|
||||
$this->assertCount($maxConnections, $lastHeartbeatTimes);
|
||||
|
||||
foreach ($connections as $connection) {
|
||||
$pool->put($connection);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function testCreateConnectionThrowsException()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$pool->setConnectionCreator(function () {
|
||||
throw new Exception('Failed to create connection');
|
||||
});
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$this->expectExceptionMessage('Failed to create connection');
|
||||
|
||||
try {
|
||||
$pool->createConnection();
|
||||
} finally {
|
||||
// 确保 currentConnections 减少
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
}
|
||||
}
|
||||
|
||||
public function testCloseConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connection = $this->createMock(ConnectionMock::class);
|
||||
|
||||
// 模拟连接属于连接池
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$connections[$connection] = time();
|
||||
|
||||
$connection->expects($this->once())->method('close');
|
||||
$pool->setConnectionCloser(function ($conn) {
|
||||
$conn->close();
|
||||
});
|
||||
|
||||
$pool->closeConnection($connection);
|
||||
|
||||
// 确保 currentConnections 减少
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
|
||||
// 确保连接从 WeakMap 中移除
|
||||
$this->assertFalse($connections->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testCloseConnections()
|
||||
{
|
||||
$maxConnections = 5;
|
||||
|
||||
$pool = new Pool($maxConnections);
|
||||
|
||||
$pool->setConnectionCreator(function () {
|
||||
$connection = $this->createMock(ConnectionMock::class);
|
||||
$connection->expects($this->once())->method('close');
|
||||
return $connection;
|
||||
});
|
||||
|
||||
$pool->setConnectionCloser(function ($conn) {
|
||||
$conn->close();
|
||||
});
|
||||
|
||||
$connections = [];
|
||||
for ($i = 0; $i < $maxConnections; $i++) {
|
||||
$connections[] = $pool->get();
|
||||
}
|
||||
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 1, $this->getCurrentConnections($pool));
|
||||
|
||||
$pool->closeConnections();
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 0, $this->getCurrentConnections($pool));
|
||||
if (!Coroutine::isCoroutine()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($connections as $connection) {
|
||||
$pool->put($connection);
|
||||
}
|
||||
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
|
||||
$pool->closeConnections();
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
|
||||
$connections = [];
|
||||
for ($i = 0; $i < $maxConnections; $i++) {
|
||||
$connections[] = $pool->get();
|
||||
}
|
||||
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
|
||||
foreach ($connections as $connection) {
|
||||
$pool->put($connection);
|
||||
}
|
||||
$pool->closeConnections();
|
||||
unset($connections);
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
}
|
||||
|
||||
public function testCloseConnectionWithExceptionInDestroyHandler()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connection = $this->createMock(stdClass::class);
|
||||
|
||||
// 模拟连接属于连接池
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$connections[$connection] = time();
|
||||
|
||||
$exception = new Exception('Error closing connection');
|
||||
|
||||
$pool->setConnectionCloser(function ($conn) use ($exception) {
|
||||
throw $exception;
|
||||
});
|
||||
|
||||
// 设置日志记录器
|
||||
$loggerMock = $this->createMock(LoggerInterface::class);
|
||||
$loggerMock->expects($this->once())
|
||||
->method('info')
|
||||
->with($this->stringContains('Error closing connection'));
|
||||
|
||||
$this->setPrivateProperty($pool, 'logger', $loggerMock);
|
||||
|
||||
$pool->closeConnection($connection);
|
||||
|
||||
// 确保 currentConnections 减少
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
|
||||
// 确保连接从 WeakMap 中移除
|
||||
$this->assertFalse($connections->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testHeartbeatChecker()
|
||||
{
|
||||
$pool = $this->getMockBuilder(Pool::class)
|
||||
->setConstructorArgs([5])
|
||||
->onlyMethods(['closeConnection'])
|
||||
->getMock();
|
||||
|
||||
$connection = $this->createMock(stdClass::class);
|
||||
|
||||
// 设置连接心跳检测器
|
||||
$pool->setHeartbeatChecker(function ($conn) {
|
||||
// 模拟心跳检测
|
||||
});
|
||||
|
||||
// 模拟连接在通道中
|
||||
$channel = $this->getPrivateProperty($pool, 'channel');
|
||||
$channel->push($connection);
|
||||
|
||||
// 设置连接的上次使用时间和心跳时间
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$connections[$connection] = time();
|
||||
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastUsedTimes[$connection] = time();
|
||||
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
$lastHeartbeatTimes[$connection] = time() - 100; // 超过心跳间隔
|
||||
|
||||
// 调用受保护的 checkConnections 方法
|
||||
$reflectedMethod = new ReflectionMethod($pool, 'checkConnections');
|
||||
$reflectedMethod->invoke($pool);
|
||||
|
||||
// 检查心跳时间是否更新
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
$this->assertGreaterThan(time() - 2, $lastHeartbeatTimes[$connection]);
|
||||
}
|
||||
|
||||
public function testConnectionDestroyedWithoutReturn()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
// 设置连接创建器
|
||||
$pool->setConnectionCreator(function () {
|
||||
return new stdClass;
|
||||
});
|
||||
|
||||
// 获取初始的 currentConnections
|
||||
$initialConnections = $this->getCurrentConnections($pool);
|
||||
|
||||
// 从连接池获取一个连接
|
||||
$connection = $pool->get();
|
||||
|
||||
// 检查 currentConnections 是否增加
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections + 1 : 1, $this->getCurrentConnections($pool));
|
||||
|
||||
// 不归还连接,并销毁连接对象
|
||||
unset($connection);
|
||||
|
||||
// 检查 currentConnections 是否减少
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections : 1, $this->getCurrentConnections($pool));
|
||||
}
|
||||
|
||||
private function getPrivateProperty($object, string $property)
|
||||
{
|
||||
$prop = new ReflectionProperty($object, $property);
|
||||
return $prop->getValue($object);
|
||||
}
|
||||
|
||||
private function setPrivateProperty($object, string $property, $value)
|
||||
{
|
||||
$prop = new ReflectionProperty($object, $property);
|
||||
$prop->setValue($object, $value);
|
||||
}
|
||||
|
||||
private function getCurrentConnections($object): int
|
||||
{
|
||||
return $object->getConnectionCount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 定义 ConnectionMock 类用于测试
|
||||
class ConnectionMock
|
||||
{
|
||||
public function close()
|
||||
{
|
||||
// 模拟关闭连接
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Coroutine\WaitGroup;
|
||||
|
||||
/**
|
||||
* Class WaitGroupTest
|
||||
*
|
||||
* Tests for the Fiber WaitGroup implementation.
|
||||
*/
|
||||
class WaitGroupTest extends TestCase
|
||||
{
|
||||
|
||||
public function testWaitWaitGroupDone()
|
||||
{
|
||||
$waitGroup = new WaitGroup();
|
||||
$this->assertEquals(0, $waitGroup->count());
|
||||
$results = [0];
|
||||
$this->assertTrue($waitGroup->add());
|
||||
Coroutine::create(function () use ($waitGroup, &$results) {
|
||||
try {
|
||||
Timer::sleep(0.1);
|
||||
$results[] = 1;
|
||||
} finally {
|
||||
$this->assertTrue($waitGroup->done());
|
||||
}
|
||||
});
|
||||
$this->assertTrue($waitGroup->add());
|
||||
Coroutine::create(function () use ($waitGroup, &$results) {
|
||||
try {
|
||||
Timer::sleep(0.2);
|
||||
$results[] = 2;
|
||||
} finally {
|
||||
$this->assertTrue($waitGroup->done());
|
||||
}
|
||||
});
|
||||
$this->assertTrue($waitGroup->add());
|
||||
Coroutine::create(function () use ($waitGroup, &$results) {
|
||||
try {
|
||||
Timer::sleep(0.3);
|
||||
$results[] = 3;
|
||||
} finally {
|
||||
$this->assertTrue($waitGroup->done());
|
||||
}
|
||||
});
|
||||
$this->assertTrue($waitGroup->wait());
|
||||
$this->assertEquals(0, $waitGroup->count(), 'WaitGroup count should be 0 after wait is called.');
|
||||
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
use Workerman\Events\Event;
|
||||
use Workerman\Events\Select;
|
||||
use Workerman\Events\Swow;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Events\Fiber;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$phpunitDisplayOptions = [
|
||||
'--colors=always',
|
||||
'--display-deprecations',
|
||||
'--display-phpunit-deprecations',
|
||||
'--display-errors',
|
||||
'--display-notices',
|
||||
'--display-warnings',
|
||||
'--display-incomplete',
|
||||
'--display-skipped',
|
||||
];
|
||||
|
||||
if (DIRECTORY_SEPARATOR === '/' || (!extension_loaded('swow') && !class_exists(Revolt\EventLoop::class))) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
__DIR__ . '/ChannelTest.php',
|
||||
__DIR__ . '/PoolTest.php',
|
||||
__DIR__ . '/BarrierTest.php',
|
||||
__DIR__ . '/ContextTest.php',
|
||||
__DIR__ . '/WaitGroupTest.php',
|
||||
]);
|
||||
}, Select::class);
|
||||
}
|
||||
|
||||
if (extension_loaded('event')) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
__DIR__ . '/ChannelTest.php',
|
||||
__DIR__ . '/PoolTest.php',
|
||||
__DIR__ . '/BarrierTest.php',
|
||||
__DIR__ . '/ContextTest.php',
|
||||
__DIR__ . '/WaitGroupTest.php',
|
||||
]);
|
||||
}, Event::class);
|
||||
}
|
||||
|
||||
if (class_exists(Revolt\EventLoop::class) && (DIRECTORY_SEPARATOR === '/' || !extension_loaded('swow'))) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
...glob(__DIR__ . '/*Test.php')
|
||||
]);
|
||||
}, Fiber::class);
|
||||
}
|
||||
|
||||
if (extension_loaded('Swoole')) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
...glob(__DIR__ . '/*Test.php')
|
||||
]);
|
||||
}, Swoole::class);
|
||||
}
|
||||
|
||||
if (extension_loaded('Swow')) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
...glob(__DIR__ . '/*Test.php')
|
||||
]);
|
||||
}, Swow::class);
|
||||
}
|
||||
|
||||
function create_test_worker(Closure $callable, $eventLoopClass): void
|
||||
{
|
||||
$worker = new Worker();
|
||||
$worker->eventLoop = $eventLoopClass;
|
||||
$worker->onWorkerStart = function () use ($callable, $eventLoopClass) {
|
||||
$fp = fopen(__FILE__, 'r+');
|
||||
flock($fp, LOCK_EX);
|
||||
echo PHP_EOL . PHP_EOL. PHP_EOL . '[TEST EVENT-LOOP: ' . basename(str_replace('\\', '/', $eventLoopClass)) . ']' . PHP_EOL;
|
||||
try {
|
||||
$callable();
|
||||
} catch (Throwable $e) {
|
||||
echo $e;
|
||||
} finally {
|
||||
flock($fp, LOCK_UN);
|
||||
}
|
||||
Timer::repeat(1, function () use ($fp) {
|
||||
if (flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
if(function_exists('posix_kill')) {
|
||||
posix_kill(posix_getppid(), SIGINT);
|
||||
} else {
|
||||
Worker::stopAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Worker::runAll();
|
||||
Reference in New Issue
Block a user