Skip to content

Commit 87c4fa4

Browse files
committed
Add tests for TCP and UDP client and server functionalities
1 parent d40e433 commit 87c4fa4

15 files changed

Lines changed: 953 additions & 67 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Socket\Tests\Integration;
6+
7+
use InitPHP\Socket\Client\TCP as TcpClient;
8+
use InitPHP\Socket\Exception\SocketConnectionException;
9+
use InitPHP\Socket\Exception\SocketException;
10+
use InitPHP\Socket\Server\TCP as TcpServer;
11+
use InitPHP\Socket\Server\UDP as UdpServer;
12+
13+
final class ConnectionErrorsTest extends IntegrationTestCase
14+
{
15+
public function testTcpClientConnectFailsWhenPortIsClosed(): void
16+
{
17+
// Bind+release a port to find one we are confident is unbound.
18+
$port = $this->findFreePort();
19+
20+
$client = new TcpClient('127.0.0.1', $port);
21+
$this->expectException(SocketConnectionException::class);
22+
$client->connect();
23+
}
24+
25+
public function testTcpServerListenFailsOnPortConflict(): void
26+
{
27+
$port = $this->findFreePort();
28+
29+
$a = new TcpServer('127.0.0.1', $port);
30+
$a->listen();
31+
$this->registerCleanup($a->close(...));
32+
33+
$b = new TcpServer('127.0.0.1', $port);
34+
$this->expectException(SocketException::class);
35+
$b->listen();
36+
}
37+
38+
public function testUdpServerListenFailsOnPortConflict(): void
39+
{
40+
$port = $this->findFreePort(\SOCK_DGRAM);
41+
42+
$a = new UdpServer('127.0.0.1', $port);
43+
$a->listen();
44+
$this->registerCleanup($a->close(...));
45+
46+
$b = new UdpServer('127.0.0.1', $port);
47+
$this->expectException(SocketException::class);
48+
$b->listen();
49+
}
50+
51+
public function testTcpClientWriteReadAfterDisconnectReturnsNull(): void
52+
{
53+
$port = $this->findFreePort();
54+
$server = new TcpServer('127.0.0.1', $port);
55+
$server->listen();
56+
$this->registerCleanup($server->close(...));
57+
58+
$client = new TcpClient('127.0.0.1', $port);
59+
$client->connect();
60+
$client->disconnect();
61+
62+
self::assertNull($client->write('x'));
63+
self::assertNull($client->read(64));
64+
self::assertNull($client->getSocket());
65+
}
66+
67+
public function testTcpServerConnectTwiceThrows(): void
68+
{
69+
$port = $this->findFreePort();
70+
$server = new TcpServer('127.0.0.1', $port);
71+
$server->listen();
72+
$this->registerCleanup($server->close(...));
73+
74+
$client = new TcpClient('127.0.0.1', $port);
75+
$client->connect();
76+
$this->registerCleanup($client->disconnect(...));
77+
78+
$this->expectException(SocketException::class);
79+
$client->connect();
80+
}
81+
}

tests/Integration/ServerLifecycleTest.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace InitPHP\Socket\Tests\Integration;
66

7+
use InitPHP\Socket\Client\TCP as TcpClient;
78
use InitPHP\Socket\Exception\SocketException;
9+
use InitPHP\Socket\Interfaces\SocketConnectionInterface;
10+
use InitPHP\Socket\Interfaces\SocketServerInterface;
811
use InitPHP\Socket\Server\TCP as TcpServer;
912

1013
final class ServerLifecycleTest extends IntegrationTestCase
@@ -55,28 +58,34 @@ public function testTickBeforeListenThrows(): void
5558
$server->tick(static fn () => null, 0.0);
5659
}
5760

58-
public function testStopExitsLiveLoopOnNextTick(): void
61+
public function testStopFromInsideCallbackExitsLiveLoop(): void
5962
{
6063
$port = $this->findFreePort();
6164
$server = new TcpServer('127.0.0.1', $port);
6265
$server->listen();
6366
$this->registerCleanup($server->close(...));
6467

65-
$iterations = 0;
66-
$callback = static function () use ($server, &$iterations): void {
67-
++$iterations;
68-
if ($iterations >= 1) {
69-
$server->stop();
70-
}
71-
};
72-
73-
// No clients ever connect, so tick() will idle out within idleSeconds.
74-
// We rely on stop() being called externally via a tiny shim that
75-
// hooks into a no-op activity: drive tick() ourselves a few times.
76-
$server->stop();
68+
$client = new TcpClient('127.0.0.1', $port);
69+
$client->connect();
70+
$this->registerCleanup($client->disconnect(...));
71+
72+
// Bring the client into the server's accept queue, then feed a byte
73+
// so the next live() iteration actually fires the callback.
74+
$server->tick(static fn () => null, 0.2);
75+
self::assertCount(1, $server->getClients());
76+
self::assertSame(4, $client->write('stop'));
77+
78+
$invocations = 0;
79+
$server->live(
80+
static function (SocketServerInterface $srv, SocketConnectionInterface $conn) use (&$invocations): void {
81+
++$invocations;
82+
$conn->read(1024);
83+
$srv->stop();
84+
},
85+
0.05,
86+
);
87+
88+
self::assertSame(1, $invocations);
7789
self::assertFalse($server->isRunning());
78-
// After stop() the live loop should return immediately.
79-
$server->live($callback, 0.01);
80-
self::assertSame(0, $iterations, 'live() should not enter the loop after stop()');
8190
}
8291
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Socket\Tests\Integration;
6+
7+
use InitPHP\Socket\Client\AbstractStreamClient;
8+
use InitPHP\Socket\Enum\Transport;
9+
use InitPHP\Socket\Exception\SocketException;
10+
11+
/**
12+
* Exercises {@see AbstractStreamClient} end-to-end against a plain `tcp://`
13+
* stream server in the same process. This skips the TLS handshake — for
14+
* which we already have {@see TlsEchoTest} — but covers every other
15+
* stream-client code path (connect, read, write, timeout, blocking,
16+
* disconnect, crypto-toggle).
17+
*/
18+
final class StreamClientIoTest extends IntegrationTestCase
19+
{
20+
public function testConnectReadWriteAgainstPlainTcpServer(): void
21+
{
22+
$port = $this->findFreePort();
23+
$errNo = 0;
24+
$errStr = '';
25+
$server = stream_socket_server("tcp://127.0.0.1:{$port}", $errNo, $errStr);
26+
self::assertNotFalse($server, "stream_socket_server failed: {$errStr}");
27+
$this->registerCleanup(static fn () => @fclose($server));
28+
29+
$client = new PlainStreamClient('127.0.0.1', $port, 2.0);
30+
$client->option('verify_peer', false)
31+
->timeout(1.5)
32+
->blocking(false);
33+
$client->connect();
34+
$this->registerCleanup($client->disconnect(...));
35+
36+
$peer = stream_socket_accept($server, 1.0);
37+
self::assertNotFalse($peer);
38+
$this->registerCleanup(static fn () => @fclose($peer));
39+
40+
self::assertSame(5, $client->write('hello'));
41+
// Allow the kernel to deliver.
42+
usleep(20_000);
43+
self::assertSame('hello', fread($peer, 1024));
44+
45+
fwrite($peer, 'world');
46+
$reply = null;
47+
for ($i = 0; $i < 30 && $reply === null; ++$i) {
48+
$reply = $client->read(1024);
49+
if ($reply === null) {
50+
usleep(10_000);
51+
}
52+
}
53+
self::assertSame('world', $reply);
54+
55+
// toggleable after connect — exercises stream_set_blocking + stream_set_timeout branches.
56+
$client->blocking(true);
57+
$client->timeout(0.5);
58+
}
59+
60+
public function testCryptoCanBeDisabledOnPlainStream(): void
61+
{
62+
$port = $this->findFreePort();
63+
$errNo = 0;
64+
$errStr = '';
65+
$server = stream_socket_server("tcp://127.0.0.1:{$port}", $errNo, $errStr);
66+
self::assertNotFalse($server);
67+
$this->registerCleanup(static fn () => @fclose($server));
68+
69+
$client = new PlainStreamClient('127.0.0.1', $port);
70+
$client->connect();
71+
$this->registerCleanup($client->disconnect(...));
72+
73+
// Disabling crypto on a non-encrypted stream is a no-op and must not throw.
74+
self::assertSame($client, $client->crypto(null));
75+
}
76+
77+
public function testConnectTwiceThrows(): void
78+
{
79+
$port = $this->findFreePort();
80+
$errNo = 0;
81+
$errStr = '';
82+
$server = stream_socket_server("tcp://127.0.0.1:{$port}", $errNo, $errStr);
83+
self::assertNotFalse($server);
84+
$this->registerCleanup(static fn () => @fclose($server));
85+
86+
$client = new PlainStreamClient('127.0.0.1', $port);
87+
$client->connect();
88+
$this->registerCleanup($client->disconnect(...));
89+
90+
$this->expectException(SocketException::class);
91+
$client->connect();
92+
}
93+
}
94+
95+
/**
96+
* Test-only subclass that drives the abstract stream client over a plain
97+
* `tcp://` scheme. Lets us cover the abstract's I/O paths in the same
98+
* process without the cost of a TLS handshake.
99+
*/
100+
final class PlainStreamClient extends AbstractStreamClient
101+
{
102+
public function __construct(string $host, int $port, ?float $timeout = null)
103+
{
104+
parent::__construct($host, $port, Transport::TCP, $timeout);
105+
}
106+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Socket\Tests\Integration;
6+
7+
use InitPHP\Socket\Client\TCP as TcpClient;
8+
use InitPHP\Socket\Server\TCP as TcpServer;
9+
10+
final class TcpDisconnectTest extends IntegrationTestCase
11+
{
12+
public function testServerEvictsClientAfterPeerDisconnects(): void
13+
{
14+
$port = $this->findFreePort();
15+
$server = new TcpServer('127.0.0.1', $port);
16+
$server->listen();
17+
$this->registerCleanup($server->close(...));
18+
19+
$client = new TcpClient('127.0.0.1', $port);
20+
$client->connect();
21+
22+
// Accept the client.
23+
$server->tick(static fn () => null, 0.2);
24+
self::assertCount(1, $server->getClients());
25+
26+
// Client drops the connection.
27+
$client->disconnect();
28+
29+
// The next tick should detect EOF on the dead socket and evict it.
30+
// We may need a couple of iterations for the kernel to surface the close.
31+
$eviction = false;
32+
for ($i = 0; $i < 20 && !$eviction; ++$i) {
33+
$server->tick(static fn () => null, 0.05);
34+
if (\count($server->getClients()) === 0) {
35+
$eviction = true;
36+
}
37+
}
38+
self::assertTrue($eviction, 'server did not evict the disconnected client');
39+
}
40+
41+
public function testBroadcastSkipsDeadClient(): void
42+
{
43+
$port = $this->findFreePort();
44+
$server = new TcpServer('127.0.0.1', $port);
45+
$server->listen();
46+
$this->registerCleanup($server->close(...));
47+
48+
$alive = new TcpClient('127.0.0.1', $port);
49+
$alive->connect();
50+
$this->registerCleanup($alive->disconnect(...));
51+
52+
$deadOnArrival = new TcpClient('127.0.0.1', $port);
53+
$deadOnArrival->connect();
54+
$deadOnArrival->disconnect();
55+
56+
// Accept both.
57+
$server->tick(static fn () => null, 0.2);
58+
$server->tick(static fn () => null, 0.2);
59+
self::assertCount(2, $server->getClients());
60+
61+
// Broadcast — the dead client write call should silently no-op and
62+
// not raise; the alive client should still receive the message.
63+
self::assertTrue($server->broadcast('beacon'));
64+
65+
$reply = null;
66+
for ($i = 0; $i < 30 && $reply === null; ++$i) {
67+
$reply = $alive->read(1024);
68+
if ($reply === null) {
69+
usleep(10_000);
70+
}
71+
}
72+
self::assertSame('beacon', $reply);
73+
}
74+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Socket\Tests\Integration;
6+
7+
use InitPHP\Socket\Client\TCP as TcpClient;
8+
use InitPHP\Socket\Server\TCP as TcpServer;
9+
10+
final class TcpServerCloseTest extends IntegrationTestCase
11+
{
12+
public function testCloseTearsDownEveryActiveClient(): void
13+
{
14+
$port = $this->findFreePort();
15+
$server = new TcpServer('127.0.0.1', $port);
16+
$server->listen();
17+
18+
$clients = [];
19+
for ($i = 0; $i < 3; ++$i) {
20+
$c = new TcpClient('127.0.0.1', $port);
21+
$c->connect();
22+
$clients[] = $c;
23+
$this->registerCleanup($c->disconnect(...));
24+
}
25+
// Accept all of them.
26+
for ($i = 0; $i < 3; ++$i) {
27+
$server->tick(static fn () => null, 0.1);
28+
}
29+
self::assertCount(3, $server->getClients());
30+
31+
self::assertTrue($server->close());
32+
self::assertSame([], $server->getClients());
33+
self::assertNull($server->getSocket());
34+
}
35+
36+
public function testCustomBacklogIsApplied(): void
37+
{
38+
$port = $this->findFreePort();
39+
$server = new TcpServer('127.0.0.1', $port);
40+
self::assertSame($server, $server->backlog(2));
41+
$server->listen();
42+
$this->registerCleanup($server->close(...));
43+
44+
// We don't introspect the OS backlog directly; we just exercise the
45+
// chained setter path and confirm listen() still works afterwards.
46+
self::assertNotNull($server->getSocket());
47+
}
48+
}

0 commit comments

Comments
 (0)