Skip to content

Commit c71fda4

Browse files
authored
Merge pull request #29 from clue-labs/coverage
Update test suite to ensure 100% code coverage
2 parents 1210598 + d0064f0 commit c71fda4

4 files changed

Lines changed: 82 additions & 13 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ jobs:
3333
php-version: ${{ matrix.php }}
3434
coverage: xdebug
3535
- run: composer install
36-
- run: vendor/bin/phpunit --coverage-text ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }}
36+
- run: vendor/bin/phpunit --coverage-text --coverage-clover clover.xml ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }}
37+
- name: Check 100% code coverage
38+
if: ${{ matrix.php >= 5.4 }}
39+
shell: php {0}
40+
run: |
41+
<?php
42+
$metrics = simplexml_load_file('clover.xml')->project->metrics;
43+
exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1);
3744
3845
PHPUnit-hhvm:
3946
name: PHPUnit (HHVM)

‎README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# clue/reactphp-multicast
22

33
[![CI status](https://github.com/clue/reactphp-multicast/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-multicast/actions)
4+
[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests)
45
[![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)
56

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

190+
The test suite is set up to always ensure 100% code coverage across all
191+
supported environments (except `createReceiver()` on legacy PHP 5.3). If you
192+
have the Xdebug extension installed, you can also generate a code coverage
193+
report locally like this:
194+
195+
```bash
196+
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
197+
```
198+
189199
## License
190200

191201
This project is released under the permissive [MIT license](LICENSE).

‎src/Factory.php‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public function __construct($loop = null)
5858
public function createSender()
5959
{
6060
$stream = @stream_socket_server('udp://0.0.0.0:0', $errno, $errstr, STREAM_SERVER_BIND);
61+
62+
// unlikely: stream_socket_server() may return false if no more file descriptors are available
6163
if ($stream === false) {
6264
throw new RuntimeException('Unable to create sending socket: ' . $errstr, $errno);
6365
}
@@ -104,10 +106,10 @@ public function createSender()
104106
public function createReceiver($address)
105107
{
106108
if (!defined('MCAST_JOIN_GROUP')) {
107-
throw new BadMethodCallException('MCAST_JOIN_GROUP not defined (requires PHP 5.4+)');
109+
throw new BadMethodCallException('MCAST_JOIN_GROUP not defined (requires PHP 5.4+)'); // @codeCoverageIgnore
108110
}
109111
if (!function_exists('socket_import_stream')) {
110-
throw new BadMethodCallException('Function socket_import_stream missing (requires ext-sockets and PHP 5.4+)');
112+
throw new BadMethodCallException('Function socket_import_stream missing (requires ext-sockets and PHP 5.4+)'); // @codeCoverageIgnore
111113
}
112114

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

120122
$socket = socket_import_stream($stream);
121-
if ($stream === false) {
122-
throw new RuntimeException('Unable to access underlying socket resource');
123-
}
124-
125-
// allow multiple processes to bind to the same address
126-
$ret = socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
127-
if ($ret === false) {
128-
throw new RuntimeException('Unable to enable SO_REUSEADDR');
129-
}
123+
assert($socket !== false);
130124

131125
set_error_handler(function () {
132126
// ignore PHP warning, error is reported via socket_last_error() below

‎tests/FunctionalTest.php‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,70 @@ public function testMultipleReceivers()
5858
$this->loop->run();
5959
}
6060

61+
public function testCreateSenderThrowsWhenFileDescriptorsAreExceeded()
62+
{
63+
$ulimit = exec('ulimit -n 2>&1', $_, $exit);
64+
if ($exit !== 0 || $ulimit < 1) {
65+
$this->markTestSkipped('Unable to determine limit of open files (ulimit not available?)');
66+
}
67+
68+
$memory = ini_get('memory_limit');
69+
if ($memory === '-1') {
70+
$memory = PHP_INT_MAX;
71+
} elseif (preg_match('/^\d+G$/i', $memory)) {
72+
$memory = ((int) $memory) * 1024 * 1024 * 1024;
73+
} elseif (preg_match('/^\d+M$/i', $memory)) {
74+
$memory = ((int) $memory) * 1024 * 1024;
75+
} elseif (preg_match('/^\d+K$/i', $memory)) {
76+
$memory = ((int) $memory) * 1024;
77+
}
78+
79+
// each file descriptor takes ~600 bytes of memory, so skip test if this would exceed memory_limit
80+
if ($ulimit * 600 > $memory || $ulimit > 100000) {
81+
$this->markTestSkipped('Test requires ~' . round($ulimit * 600 / 1024 / 1024) . '/' . round($memory / 1024 / 1024) . ' MiB memory with ' . $ulimit . ' file descriptors');
82+
}
83+
84+
// keep creating dummy file handles until all file descriptors are exhausted
85+
$fds = array();
86+
for ($i = 0; $i < $ulimit; ++$i) {
87+
$fd = @fopen('/dev/null', 'r');
88+
if ($fd === false) {
89+
break;
90+
}
91+
$fds[] = $fd;
92+
}
93+
94+
$this->setExpectedException('RuntimeException', 'Unable to create sending socket: ');
95+
$this->factory->createSender();
96+
}
97+
98+
public function testCreateReceiverThrowsForPortAlreadyInUse()
99+
{
100+
if (!defined('MCAST_JOIN_GROUP') || !function_exists('socket_import_stream')) {
101+
$this->markTestSkipped('No multicast support');
102+
}
103+
104+
// bind to random port without SO_REUSEADDR, so receiver can not bind to same port
105+
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
106+
socket_bind($socket, '0.0.0.0', 0);
107+
socket_getsockname($socket, $host, $port);
108+
109+
$this->setExpectedException('RuntimeException', 'Unable to create receiving socket: ' . socket_strerror(SOCKET_EADDRINUSE));
110+
$this->factory->createReceiver('224.224.244.244:' . $port);
111+
}
112+
61113
public function testCreateReceiverThrowsForNonMulticastAddressWithoutCallingCustomErrorHandler()
62114
{
63115
if (!defined('MCAST_JOIN_GROUP') || !function_exists('socket_import_stream')) {
64116
$this->markTestSkipped('No multicast support');
65117
}
66118

119+
// find random free port, as createReceiver() can not parse port 0 on legacy PHP
120+
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
121+
socket_bind($socket, '0.0.0.0', 0);
122+
socket_getsockname($socket, $host, $port);
123+
socket_close($socket);
124+
67125
$error = null;
68126
set_error_handler(function ($_, $errstr) use (&$error) {
69127
$error = $errstr;
@@ -72,7 +130,7 @@ public function testCreateReceiverThrowsForNonMulticastAddressWithoutCallingCust
72130
$this->setExpectedException('RuntimeException', 'Unable to join multicast group: ' . socket_strerror(SOCKET_EINVAL), SOCKET_EINVAL);
73131

74132
try {
75-
$this->factory->createReceiver('127.0.0.1:2244');
133+
$this->factory->createReceiver('127.0.0.1:' . $port);
76134

77135
restore_error_handler();
78136
} catch (\Exception $e) {

0 commit comments

Comments
 (0)