Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Io/Poll/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ never reported, and a TCP half-close reports `Read|HangUp` where native
reports plain `Read`. The phpt tests of the native implementation, borrowed
from php-src, run against the polyfill as part of the test suite.

`Context::wait()` takes its timeout as a `Time\Duration`. That class is not
required to call `wait()` without a timeout; install `symfony/polyfill-time`
to build one on PHP < 8.6.

More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).

Expand Down
21 changes: 9 additions & 12 deletions src/Io/Poll/Resources/stubs/Io/Poll/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,18 @@ public function add(Handle $handle, array $events, mixed $data = null): Watcher
return $this->watchers[$id] = $create(\WeakReference::create($this), $handle, $events, $data);
}

public function wait(?int $timeoutSeconds = null, int $timeoutMicroseconds = 0, ?int $maxEvents = null): array
public function wait(?\Time\Duration $timeout = null, ?int $maxEvents = null): array
{
if (null !== $timeoutSeconds) {
if ($timeoutSeconds < 0) {
throw new \ValueError(\sprintf('%s(): Argument #1 ($timeoutSeconds) must be greater than or equal to 0', __METHOD__));
}
if ($timeoutMicroseconds < 0) {
throw new \ValueError(\sprintf('%s(): Argument #2 ($timeoutMicroseconds) must be greater than or equal to 0', __METHOD__));
}
if (null !== $timeout && $timeout->negative) {
throw new \ValueError(\sprintf('%s(): Argument #1 ($timeout) must not be negative', __METHOD__));
}

if (null !== $maxEvents && $maxEvents <= 0) {
throw new \ValueError(\sprintf('%s(): Argument #3 ($maxEvents) must be greater than 0', __METHOD__));
throw new \ValueError(\sprintf('%s(): Argument #2 ($maxEvents) must be greater than 0', __METHOD__));
}

$timeoutNanoseconds = null !== $timeout ? $timeout->seconds * 1_000_000_000 + $timeout->nanoseconds : null;

// Like poll() reporting POLLNVAL, drop watchers whose stream has been
// closed; poll() returns immediately in that case, without sleeping
$evicted = false;
Expand All @@ -124,7 +121,7 @@ public function wait(?int $timeoutSeconds = null, int $timeoutMicroseconds = 0,
}

if (!$this->watchers) {
if (!$evicted && null !== $timeoutSeconds && 0 < $micros = $timeoutSeconds * 1_000_000 + $timeoutMicroseconds) {
if (!$evicted && null !== $timeoutNanoseconds && 0 < $micros = \intdiv($timeoutNanoseconds, 1000)) {
usleep($micros);
}

Expand All @@ -147,8 +144,8 @@ public function wait(?int $timeoutSeconds = null, int $timeoutMicroseconds = 0,

if ($evicted) {
$deadline = 0;
} elseif (null !== $timeoutSeconds) {
$deadline = hrtime(true) + ($timeoutSeconds * 1_000_000 + $timeoutMicroseconds) * 1000;
} elseif (null !== $timeoutNanoseconds) {
$deadline = hrtime(true) + $timeoutNanoseconds;
} else {
$deadline = null;
}
Expand Down
63 changes: 28 additions & 35 deletions tests/Io/Poll/PollTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Io\Poll\PollException;
use Io\Poll\Watcher;
use PHPUnit\Framework\TestCase;
use Time\Duration;

/**
* @requires PHP >= 8.1
Expand Down Expand Up @@ -319,28 +320,20 @@ public function testContextAddInvalidEventsThrows()
}
}

public function testWaitNegativeTimeoutSecondsThrows()
public function testWaitNegativeTimeoutThrows()
{
$context = new Context();
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('Io\\Poll\\Context::wait(): Argument #1 ($timeoutSeconds) must be greater than or equal to 0');
$context->wait(-1);
}

public function testWaitNegativeTimeoutMicrosecondsThrows()
{
$context = new Context();
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('Io\\Poll\\Context::wait(): Argument #2 ($timeoutMicroseconds) must be greater than or equal to 0');
$context->wait(0, -1);
$this->expectExceptionMessage('Io\\Poll\\Context::wait(): Argument #1 ($timeout) must not be negative');
$context->wait(Duration::fromSeconds(1)->negate());
}

public function testWaitNonPositiveMaxEventsThrows()
{
$context = new Context();
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('Io\\Poll\\Context::wait(): Argument #3 ($maxEvents) must be greater than 0');
$context->wait(0, 0, 0);
$this->expectExceptionMessage('Io\\Poll\\Context::wait(): Argument #2 ($maxEvents) must be greater than 0');
$context->wait(Duration::fromSeconds(0), 0);
}

public function testWaitThrowsWhenInterruptedBySignal()
Expand All @@ -360,7 +353,7 @@ public function testWaitThrowsWhenInterruptedBySignal()
$this->expectException(FailedPollWaitException::class);
$this->expectExceptionMessage('Poll wait failed');
try {
$context->wait(10);
$context->wait(Duration::fromSeconds(10));
} finally {
pcntl_alarm(0);
pcntl_signal_dispatch();
Expand All @@ -370,14 +363,14 @@ public function testWaitThrowsWhenInterruptedBySignal()
}
}

public function testWaitNullTimeoutIgnoresNegativeMicroseconds()
public function testWaitNullTimeoutWaitsForReadiness()
{
[$r, $w] = stream_socket_pair(\STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP);
fwrite($w, 'hello');
$context = new Context();
$watcher = $context->add(new \StreamPollHandle($r), [Event::Read]);

$result = $context->wait(null, -1);
$result = $context->wait();

$this->assertSame([$watcher], $result);

Expand All @@ -392,7 +385,7 @@ public function testWaitImmediateTimeoutReturnsEmpty()
$context = new Context();
$context->add($handle, [Event::Read]);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertSame([], $result);
fclose($r);
Expand All @@ -408,7 +401,7 @@ public function testWaitDetectsReadable()
$handle = new \StreamPollHandle($r);
$watcher = $context->add($handle, [Event::Read]);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertCount(1, $result);
$this->assertSame($watcher, $result[0]);
Expand All @@ -427,7 +420,7 @@ public function testWaitDetectsWritable()
$handle = new \StreamPollHandle($w);
$watcher = $context->add($handle, [Event::Write]);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertCount(1, $result);
$this->assertSame($watcher, $result[0]);
Expand All @@ -445,7 +438,7 @@ public function testWaitBlocksUntilTimeoutWithNoEvents()
$context->add($handle, [Event::Read]);

$start = hrtime(true);
$result = $context->wait(0, 50000);
$result = $context->wait(Duration::fromMicroseconds(50000));
$elapsed = (hrtime(true) - $start) / 1000000;

$this->assertSame([], $result);
Expand All @@ -464,7 +457,7 @@ public function testWaitDoesNotReturnEarlyOnUnwatchedReadiness()
$context->add(new \StreamPollHandle($r), [Event::HangUp]);

$start = hrtime(true);
$result = $context->wait(0, 60000);
$result = $context->wait(Duration::fromMicroseconds(60000));
$elapsed = (hrtime(true) - $start) / 1000000;

$this->assertSame([], $result);
Expand All @@ -484,7 +477,7 @@ public function testWaitReportsReadAndHangUpOnClosedSocketPeer()

fclose($w);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertSame([$watcher], $result);
$this->assertSame([Event::Read, Event::HangUp], $watcher->getTriggeredEvents());
Expand All @@ -501,7 +494,7 @@ public function testWaitReportsHangUpEvenWhenNotWatched()

fclose($w);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertSame([$watcher], $result);
$this->assertSame([Event::HangUp], $watcher->getTriggeredEvents());
Expand All @@ -520,7 +513,7 @@ public function testWaitReportsReadOnRegularFileAtEof()
$context = new Context();
$watcher = $context->add(new \StreamPollHandle($stream), [Event::Read]);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertSame([$watcher], $result);
$this->assertTrue($watcher->hasTriggered(Event::Read));
Expand All @@ -543,7 +536,7 @@ public function testWaitReportsHangUpOnPipeAtEof()
$context = new Context();
$watcher = $context->add(new \StreamPollHandle($pipe), [Event::Read]);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertSame([$watcher], $result);
$this->assertTrue($watcher->hasTriggered(Event::HangUp));
Expand All @@ -561,7 +554,7 @@ public function testWaitReportsReadForEmptyUdpDatagram()
$context = new Context();
$watcher = $context->add(new \StreamPollHandle($server), [Event::Read]);

$result = $context->wait(1, 0);
$result = $context->wait(Duration::fromSeconds(1));

$this->assertSame([$watcher], $result);
$this->assertTrue($watcher->hasTriggered(Event::Read));
Expand All @@ -581,7 +574,7 @@ public function testWaitReportsReadOnReadableNonSocketStream()
$context = new Context();
$watcher = $context->add(new \StreamPollHandle($stream), [Event::Read]);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));

$this->assertSame([$watcher], $result);
$this->assertTrue($watcher->hasTriggered(Event::Read));
Expand All @@ -598,12 +591,12 @@ public function testWaitRetainsTriggeredEventsWhenNotRetriggered()
$context = new Context();
$watcher = $context->add(new \StreamPollHandle($r), [Event::Read]);

$context->wait(0, 0);
$context->wait(Duration::fromSeconds(0));
$this->assertSame([Event::Read], $watcher->getTriggeredEvents());

fread($r, 5);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));
$this->assertSame([], $result);
$this->assertSame([Event::Read], $watcher->getTriggeredEvents());

Expand All @@ -622,7 +615,7 @@ public function testWaitMaxEventsLimitsReturn()
$context->add(new \StreamPollHandle($r1), [Event::Read]);
$context->add(new \StreamPollHandle($r2), [Event::Read]);

$result = $context->wait(0, 0, 1);
$result = $context->wait(Duration::fromSeconds(0), 1);
$this->assertCount(1, $result);

fclose($r1);
Expand All @@ -641,15 +634,15 @@ public function testWaitReturnsImmediatelyWhenAllHandlesClosed()
fclose($w);

$start = hrtime(true);
$result = $context->wait(5, 0);
$result = $context->wait(Duration::fromSeconds(5));
$elapsed = (hrtime(true) - $start) / 1000000;

$this->assertSame([], $result);
$this->assertLessThan(1000, $elapsed);
$this->assertTrue($watcher->isActive());

$start = hrtime(true);
$result = $context->wait(0, 50000);
$result = $context->wait(Duration::fromMicroseconds(50000));
$elapsed = (hrtime(true) - $start) / 1000000;

$this->assertSame([], $result);
Expand All @@ -665,11 +658,11 @@ public function testWaitOneShotKeepsWatcherActive()
$handle = new \StreamPollHandle($r);
$watcher = $context->add($handle, [Event::Read, Event::OneShot]);

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));
$this->assertCount(1, $result);
$this->assertTrue($watcher->isActive());

$result = $context->wait(0, 0);
$result = $context->wait(Duration::fromSeconds(0));
$this->assertSame([], $result);

try {
Expand Down Expand Up @@ -936,7 +929,7 @@ public function testTimeoutMicroOverflowIntoSeconds()
$context->add($handle, [Event::Read]);

$start = hrtime(true);
$context->wait(0, 1100000);
$context->wait(Duration::fromMicroseconds(1100000));
$elapsed = (hrtime(true) - $start) / 1000000;

$this->assertGreaterThanOrEqual(1000, $elapsed);
Expand Down
24 changes: 24 additions & 0 deletions tests/Io/Poll/phpt/poll_ctx_wait.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
Io\Poll\Context::wait(): Parameter validation
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

$poll_ctx = new Io\Poll\Context();

try {
$poll_ctx->wait(timeout: Time\Duration::fromSeconds(1)->negate());
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
$poll_ctx->wait(maxEvents: -1);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

?>
--EXPECT--
ValueError: Io\Poll\Context::wait(): Argument #1 ($timeout) must not be negative
ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be greater than 0
2 changes: 1 addition & 1 deletion tests/Io/Poll/phpt/poll_stream_sock_modify_write.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ $poll_ctx = pt_new_stream_poll();
$watcher = pt_stream_poll_add($poll_ctx, $socket2, [Io\Poll\Event::Write], "socket_data");
$watcher->modify([Io\Poll\Event::Write], "modified_data");

pt_expect_events($poll_ctx->wait(0), [
pt_expect_events($poll_ctx->wait(Time\Duration::fromSeconds(0)), [
['events' => [Io\Poll\Event::Write], 'data' => 'modified_data']
]);
?>
Expand Down
2 changes: 1 addition & 1 deletion tests/Io/Poll/phpt/poll_stream_sock_read.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ $poll_ctx = pt_new_stream_poll();
pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read], "socket_data");

fwrite($socket1w, "test data");
pt_expect_events($poll_ctx->wait(0, 100000), [
pt_expect_events($poll_ctx->wait(Time\Duration::fromMicroseconds(100000)), [
['events' => [Io\Poll\Event::Read], 'data' => 'socket_data', 'read' => 'test data']
]);

Expand Down
4 changes: 2 additions & 2 deletions tests/Io/Poll/phpt/poll_stream_sock_remove_write.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ $poll_ctx = pt_new_stream_poll();
$watcher1w = pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket_data_1");
pt_stream_poll_add($poll_ctx, $socket2w, [Io\Poll\Event::Write], "socket_data_2");

pt_expect_events($poll_ctx->wait(0), [
pt_expect_events($poll_ctx->wait(Time\Duration::fromSeconds(0)), [
['events' => [Io\Poll\Event::Write], 'data' => 'socket_data_1'],
['events' => [Io\Poll\Event::Write], 'data' => 'socket_data_2']
]);

$watcher1w->remove();

pt_expect_events($poll_ctx->wait(0), [
pt_expect_events($poll_ctx->wait(Time\Duration::fromSeconds(0)), [
['events' => [Io\Poll\Event::Write], 'data' => 'socket_data_2']
]);

Expand Down
4 changes: 2 additions & 2 deletions tests/Io/Poll/phpt/poll_stream_sock_rw_close.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket2_data")
fwrite($socket1w, "test data");

fclose($socket1r);
pt_expect_events($poll_ctx->wait(0, 100000), [
pt_expect_events($poll_ctx->wait(Time\Duration::fromMicroseconds(100000)), [
[
'events' => [
'default' => [Io\Poll\Event::Write, Io\Poll\Event::Error, Io\Poll\Event::HangUp],
Expand All @@ -24,7 +24,7 @@ pt_expect_events($poll_ctx->wait(0, 100000), [
], $poll_ctx);

fclose($socket1w);
pt_expect_events($poll_ctx->wait(0, 100000), []);
pt_expect_events($poll_ctx->wait(Time\Duration::fromMicroseconds(100000)), []);

?>
--EXPECT--
Expand Down
Loading