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
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ jobs:
php-version: ${{ matrix.php }}
coverage: xdebug
- run: composer install
- run: vendor/bin/phpunit --coverage-text ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }}
- run: vendor/bin/phpunit --coverage-text --coverage-clover clover.xml ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }}
- name: Check 100% code coverage
if: ${{ matrix.php >= 5.4 }}
shell: php {0}
run: |
<?php
$metrics = simplexml_load_file('clover.xml')->project->metrics;
exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1);
Comment thread
clue marked this conversation as resolved.

PHPUnit-hhvm:
name: PHPUnit (HHVM)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# clue/reactphp-multicast

[![CI status](https://github.com/clue/reactphp-multicast/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-multicast/actions)
[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/multicast-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/multicast-react)

Simple, event-driven multicast UDP message client and server for [ReactPHP](https://reactphp.org/).
Expand Down Expand Up @@ -186,6 +187,15 @@ To run the test suite, go to the project root and run:
vendor/bin/phpunit
```

The test suite is set up to always ensure 100% code coverage across all
supported environments (except `createReceiver()` on legacy PHP 5.3). If you
have the Xdebug extension installed, you can also generate a code coverage
report locally like this:

```bash
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
```

## License

This project is released under the permissive [MIT license](LICENSE).
Expand Down
16 changes: 5 additions & 11 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public function __construct($loop = null)
public function createSender()
{
$stream = @stream_socket_server('udp://0.0.0.0:0', $errno, $errstr, STREAM_SERVER_BIND);

// unlikely: stream_socket_server() may return false if no more file descriptors are available
if ($stream === false) {
throw new RuntimeException('Unable to create sending socket: ' . $errstr, $errno);
}
Expand Down Expand Up @@ -104,10 +106,10 @@ public function createSender()
public function createReceiver($address)
{
if (!defined('MCAST_JOIN_GROUP')) {
throw new BadMethodCallException('MCAST_JOIN_GROUP not defined (requires PHP 5.4+)');
throw new BadMethodCallException('MCAST_JOIN_GROUP not defined (requires PHP 5.4+)'); // @codeCoverageIgnore
}
if (!function_exists('socket_import_stream')) {
throw new BadMethodCallException('Function socket_import_stream missing (requires ext-sockets and PHP 5.4+)');
throw new BadMethodCallException('Function socket_import_stream missing (requires ext-sockets and PHP 5.4+)'); // @codeCoverageIgnore
}

$parts = parse_url('udp://' . $address);
Expand All @@ -118,15 +120,7 @@ public function createReceiver($address)
}

$socket = socket_import_stream($stream);
if ($stream === false) {
throw new RuntimeException('Unable to access underlying socket resource');
}

// allow multiple processes to bind to the same address
$ret = socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
if ($ret === false) {
throw new RuntimeException('Unable to enable SO_REUSEADDR');
}
assert($socket !== false);

set_error_handler(function () {
// ignore PHP warning, error is reported via socket_last_error() below
Expand Down
60 changes: 59 additions & 1 deletion tests/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,70 @@ public function testMultipleReceivers()
$this->loop->run();
}

public function testCreateSenderThrowsWhenFileDescriptorsAreExceeded()
{
$ulimit = exec('ulimit -n 2>&1', $_, $exit);
if ($exit !== 0 || $ulimit < 1) {
$this->markTestSkipped('Unable to determine limit of open files (ulimit not available?)');
}

$memory = ini_get('memory_limit');
if ($memory === '-1') {
$memory = PHP_INT_MAX;
} elseif (preg_match('/^\d+G$/i', $memory)) {
$memory = ((int) $memory) * 1024 * 1024 * 1024;
} elseif (preg_match('/^\d+M$/i', $memory)) {
$memory = ((int) $memory) * 1024 * 1024;
} elseif (preg_match('/^\d+K$/i', $memory)) {
$memory = ((int) $memory) * 1024;
}

// each file descriptor takes ~600 bytes of memory, so skip test if this would exceed memory_limit
if ($ulimit * 600 > $memory || $ulimit > 100000) {
$this->markTestSkipped('Test requires ~' . round($ulimit * 600 / 1024 / 1024) . '/' . round($memory / 1024 / 1024) . ' MiB memory with ' . $ulimit . ' file descriptors');
}

// keep creating dummy file handles until all file descriptors are exhausted
$fds = array();
for ($i = 0; $i < $ulimit; ++$i) {
$fd = @fopen('/dev/null', 'r');
if ($fd === false) {
break;
}
$fds[] = $fd;
}

$this->setExpectedException('RuntimeException', 'Unable to create sending socket: ');
$this->factory->createSender();
}

public function testCreateReceiverThrowsForPortAlreadyInUse()
{
if (!defined('MCAST_JOIN_GROUP') || !function_exists('socket_import_stream')) {
$this->markTestSkipped('No multicast support');
}

// bind to random port without SO_REUSEADDR, so receiver can not bind to same port
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_bind($socket, '0.0.0.0', 0);
socket_getsockname($socket, $host, $port);

$this->setExpectedException('RuntimeException', 'Unable to create receiving socket: ' . socket_strerror(SOCKET_EADDRINUSE));
$this->factory->createReceiver('224.224.244.244:' . $port);
}

public function testCreateReceiverThrowsForNonMulticastAddressWithoutCallingCustomErrorHandler()
{
if (!defined('MCAST_JOIN_GROUP') || !function_exists('socket_import_stream')) {
$this->markTestSkipped('No multicast support');
}

// find random free port, as createReceiver() can not parse port 0 on legacy PHP
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_bind($socket, '0.0.0.0', 0);
socket_getsockname($socket, $host, $port);
socket_close($socket);

$error = null;
set_error_handler(function ($_, $errstr) use (&$error) {
$error = $errstr;
Expand All @@ -72,7 +130,7 @@ public function testCreateReceiverThrowsForNonMulticastAddressWithoutCallingCust
$this->setExpectedException('RuntimeException', 'Unable to join multicast group: ' . socket_strerror(SOCKET_EINVAL), SOCKET_EINVAL);

try {
$this->factory->createReceiver('127.0.0.1:2244');
$this->factory->createReceiver('127.0.0.1:' . $port);

restore_error_handler();
} catch (\Exception $e) {
Expand Down