From 4ff7bbcc645528cb4f442e41d6a4b34bd007ffed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Fri, 1 May 2026 21:47:18 +0200 Subject: [PATCH 1/6] M1I06: Add ConnectionTest with coverage for all methods and closed-socket paths --- tests/Server/Socket/ConnectionTest.php | 257 +++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tests/Server/Socket/ConnectionTest.php diff --git a/tests/Server/Socket/ConnectionTest.php b/tests/Server/Socket/ConnectionTest.php new file mode 100644 index 0000000..a2e45fa --- /dev/null +++ b/tests/Server/Socket/ConnectionTest.php @@ -0,0 +1,257 @@ +assertNotFalse($srv); + \socket_set_option($srv, SOL_SOCKET, SO_REUSEADDR, 1); + \socket_bind($srv, '127.0.0.1', 0); + \socket_listen($srv); + + \socket_getsockname($srv, $addr, $port); + + $client = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($client); + /** @var string $addr */ + /** @var int $port */ + \socket_connect($client, $addr, $port); + + $accepted = \socket_accept($srv); + $this->assertNotFalse($accepted); + + \socket_close($srv); + + $connection = new Connection($accepted); + + return [$connection, $client]; + } + + public function testReadReturnsString(): void + { + /** @var Connection $connection */ + /** @var \Socket $client */ + [$connection, $client] = $this->createConnectedPair(); + + \socket_write($client, 'hello', 5); + + $data = $connection->read(5); + + $this->assertSame('hello', $data); + + \socket_close($client); + $connection->close(); + } + + public function testReadReturnsFalseWhenClosed(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + $connection->close(); + + $this->assertFalse($connection->read()); + } + + public function testWriteReturnsInt(): void + { + /** @var Connection $connection */ + /** @var \Socket $client */ + [$connection, $client] = $this->createConnectedPair(); + + $result = $connection->write('world'); + + $this->assertSame(5, $result); + + \socket_close($client); + $connection->close(); + } + + public function testWriteReturnsFalseWhenClosed(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + $connection->close(); + + $this->assertFalse($connection->write('data')); + } + + public function testCloseIsIdempotent(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + + $connection->close(); + $connection->close(); + + $this->assertTrue($connection->isClosed()); + } + + public function testIsClosedInitiallyFalse(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + + $this->assertFalse($connection->isClosed()); + + \socket_close($resource); + } + + public function testIsClosedReturnsTrueAfterClose(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + $connection->close(); + + $this->assertTrue($connection->isClosed()); + } + + public function testGetPeerNameReturnsHostAndPort(): void + { + /** @var Connection $connection */ + /** @var \Socket $client */ + [$connection, $client] = $this->createConnectedPair(); + + $peer = $connection->getPeerName(); + + $this->assertArrayHasKey('host', $peer); + $this->assertArrayHasKey('port', $peer); + $this->assertSame('127.0.0.1', $peer['host']); + + \socket_close($client); + $connection->close(); + } + + public function testGetPeerNameThrowsWhenClosed(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + $connection->close(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Connection is closed'); + + $connection->getPeerName(); + } + + public function testGetLastActivityUpdatedOnRead(): void + { + /** @var Connection $connection */ + /** @var \Socket $client */ + [$connection, $client] = $this->createConnectedPair(); + + $before = $connection->getLastActivity(); + \usleep(1); + + \socket_write($client, 'x', 1); + $connection->read(1); + + $after = $connection->getLastActivity(); + + $this->assertGreaterThan($before, $after); + + \socket_close($client); + $connection->close(); + } + + public function testGetLastActivityUpdatedOnWrite(): void + { + /** @var Connection $connection */ + /** @var \Socket $client */ + [$connection, $client] = $this->createConnectedPair(); + + $before = $connection->getLastActivity(); + \usleep(1); + + $connection->write('x'); + + $after = $connection->getLastActivity(); + + $this->assertGreaterThan($before, $after); + + \socket_close($client); + $connection->close(); + } + + public function testGetLastActivityNotUpdatedOnFailedRead(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + $connection->close(); + + $before = $connection->getLastActivity(); + \usleep(1); + + $connection->read(); + + $this->assertSame($before, $connection->getLastActivity()); + } + + public function testGetLastActivityNotUpdatedOnFailedWrite(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + $connection->close(); + + $before = $connection->getLastActivity(); + \usleep(1); + + $connection->write('x'); + + $this->assertSame($before, $connection->getLastActivity()); + } + + public function testSetOptionSilentlyReturnsWhenClosed(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + $connection->close(); + + $connection->setOption(SOL_SOCKET, SO_RCVTIMEO, 1); + + $this->assertTrue($connection->isClosed()); + } + + public function testDefaultReadLength(): void + { + /** @var Connection $connection */ + /** @var \Socket $client */ + [$connection, $client] = $this->createConnectedPair(); + + \socket_write($client, str_repeat('x', 70000), 70000); + + $data = $connection->read(); + + $this->assertIsString($data); + $this->assertGreaterThan(0, strlen($data)); + + \socket_close($client); + $connection->close(); + } +} From 209cfd639864bdde4a56a93af2aed03525f47321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Fri, 1 May 2026 21:54:40 +0200 Subject: [PATCH 2/6] M1I06: Address review feedback - fix flaky sleep, check connect return, consistent cleanup --- tests/Server/Socket/ConnectionTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Server/Socket/ConnectionTest.php b/tests/Server/Socket/ConnectionTest.php index a2e45fa..92aa141 100644 --- a/tests/Server/Socket/ConnectionTest.php +++ b/tests/Server/Socket/ConnectionTest.php @@ -24,7 +24,7 @@ private function createConnectedPair(): array $this->assertNotFalse($client); /** @var string $addr */ /** @var int $port */ - \socket_connect($client, $addr, $port); + $this->assertNotFalse(\socket_connect($client, $addr, $port)); $accepted = \socket_accept($srv); $this->assertNotFalse($accepted); @@ -110,7 +110,7 @@ public function testIsClosedInitiallyFalse(): void $this->assertFalse($connection->isClosed()); - \socket_close($resource); + $connection->close(); } public function testIsClosedReturnsTrueAfterClose(): void @@ -161,7 +161,7 @@ public function testGetLastActivityUpdatedOnRead(): void [$connection, $client] = $this->createConnectedPair(); $before = $connection->getLastActivity(); - \usleep(1); + \usleep(10_000); \socket_write($client, 'x', 1); $connection->read(1); @@ -181,7 +181,7 @@ public function testGetLastActivityUpdatedOnWrite(): void [$connection, $client] = $this->createConnectedPair(); $before = $connection->getLastActivity(); - \usleep(1); + \usleep(10_000); $connection->write('x'); From 0d0e8cfa6a4963f790ba664d1a8af85a3b312c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Fri, 1 May 2026 22:09:06 +0200 Subject: [PATCH 3/6] M1I06: Fix setOption closed test - use SO_REUSEADDR, add explicit exception assertion --- tests/Server/Socket/ConnectionTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Server/Socket/ConnectionTest.php b/tests/Server/Socket/ConnectionTest.php index 92aa141..922e980 100644 --- a/tests/Server/Socket/ConnectionTest.php +++ b/tests/Server/Socket/ConnectionTest.php @@ -233,8 +233,14 @@ public function testSetOptionSilentlyReturnsWhenClosed(): void $connection = new Connection($resource); $connection->close(); - $connection->setOption(SOL_SOCKET, SO_RCVTIMEO, 1); - + $exception = null; + try { + $connection->setOption(SOL_SOCKET, SO_REUSEADDR, 1); + } catch (\Throwable $e) { + $exception = $e; + } + + $this->assertNull($exception, 'setOption should not throw when connection is closed'); $this->assertTrue($connection->isClosed()); } From 36ad65c87bc02c014a5bec754b5c4a44d9cd64cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Fri, 1 May 2026 22:14:01 +0200 Subject: [PATCH 4/6] M1I06: Add throw-path tests for getPeerName and setOption, suppress socket warnings in Connection --- src/Server/Socket/Connection.php | 4 ++-- tests/Server/Socket/ConnectionTest.php | 29 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Server/Socket/Connection.php b/src/Server/Socket/Connection.php index 2382f59..450f28c 100644 --- a/src/Server/Socket/Connection.php +++ b/src/Server/Socket/Connection.php @@ -64,7 +64,7 @@ public function getPeerName(): array throw new \RuntimeException('Connection is closed'); } - if (!\socket_getpeername($this->resource, $addr, $port)) { + if (!@\socket_getpeername($this->resource, $addr, $port)) { throw new \RuntimeException('Failed to get peer name'); } @@ -85,7 +85,7 @@ public function setOption(int $level, int $option, array|int|string $value): voi return; } - if (\socket_set_option($this->resource, $level, $option, $value) === false) { + if (@\socket_set_option($this->resource, $level, $option, $value) === false) { throw new \RuntimeException( \socket_strerror(\socket_last_error($this->resource)), ); diff --git a/tests/Server/Socket/ConnectionTest.php b/tests/Server/Socket/ConnectionTest.php index 922e980..52fb7a3 100644 --- a/tests/Server/Socket/ConnectionTest.php +++ b/tests/Server/Socket/ConnectionTest.php @@ -154,6 +154,21 @@ public function testGetPeerNameThrowsWhenClosed(): void $connection->getPeerName(); } + public function testGetPeerNameThrowsOnFailedSocketCall(): void + { + $resource = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Failed to get peer name'); + + $connection->getPeerName(); + + \socket_close($resource); + } + public function testGetLastActivityUpdatedOnRead(): void { /** @var Connection $connection */ @@ -244,6 +259,20 @@ public function testSetOptionSilentlyReturnsWhenClosed(): void $this->assertTrue($connection->isClosed()); } + public function testSetOptionThrowsOnFailedSocketCall(): void + { + $resource = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($resource); + + $connection = new Connection($resource); + + $this->expectException(\RuntimeException::class); + + $connection->setOption(SOL_SOCKET, SO_RCVBUF, -1); + + \socket_close($resource); + } + public function testDefaultReadLength(): void { /** @var Connection $connection */ From 19ee8f7db35a03414075b661f4960eee2150ab17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Fri, 1 May 2026 22:15:28 +0200 Subject: [PATCH 5/6] M1I06: Add comments explaining @ suppression on socket functions --- src/Server/Socket/Connection.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Server/Socket/Connection.php b/src/Server/Socket/Connection.php index 450f28c..ca5c647 100644 --- a/src/Server/Socket/Connection.php +++ b/src/Server/Socket/Connection.php @@ -64,6 +64,7 @@ public function getPeerName(): array throw new \RuntimeException('Connection is closed'); } + // @ because socket_getpeername emits a warning on failure; we throw our own exception if (!@\socket_getpeername($this->resource, $addr, $port)) { throw new \RuntimeException('Failed to get peer name'); } @@ -85,6 +86,7 @@ public function setOption(int $level, int $option, array|int|string $value): voi return; } + // @ because socket_set_option emits a warning on failure; we throw our own exception if (@\socket_set_option($this->resource, $level, $option, $value) === false) { throw new \RuntimeException( \socket_strerror(\socket_last_error($this->resource)), From e1fb9a492e79a3294430d427c5c9c38f34f397e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Fri, 1 May 2026 22:19:45 +0200 Subject: [PATCH 6/6] M1I06: Fix setOption throw test - use SOL_SOCKET,99999 (universally invalid option) --- tests/Server/Socket/ConnectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Server/Socket/ConnectionTest.php b/tests/Server/Socket/ConnectionTest.php index 52fb7a3..aaa2314 100644 --- a/tests/Server/Socket/ConnectionTest.php +++ b/tests/Server/Socket/ConnectionTest.php @@ -268,7 +268,7 @@ public function testSetOptionThrowsOnFailedSocketCall(): void $this->expectException(\RuntimeException::class); - $connection->setOption(SOL_SOCKET, SO_RCVBUF, -1); + $connection->setOption(SOL_SOCKET, 99999, 0); \socket_close($resource); }